初始化项目,自 v1.7.1 版本开始

This commit is contained in:
YunaiV
2023-02-11 00:44:00 +08:00
parent 11161afc1a
commit 56f3017baa
548 changed files with 52096 additions and 61 deletions

View File

@@ -0,0 +1,76 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
sort: [required],
email: [required],
phone: [
{
len: 11,
trigger: 'blur',
message: '请输入正确的手机号码'
}
]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: null,
action: true,
columns: [
{
title: '上级部门',
field: 'parentId',
isTable: false
},
{
title: '部门名称',
field: 'name',
isSearch: true,
table: {
treeNode: true,
align: 'left'
}
},
{
title: '负责人',
field: 'leaderUserId',
table: {
slots: {
default: 'leaderUserId_default'
}
}
},
{
title: '联系电话',
field: 'phone'
},
{
title: '邮箱',
field: 'email',
isTable: false
},
{
title: '显示排序',
field: 'sort'
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,189 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable ref="xGrid" @register="registerTable" show-overflow>
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:dept:create']"
@click="handleCreate()"
/>
<XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
<XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
</template>
<template #leaderUserId_default="{ row }">
<span>{{ userNicknameFormat(row) }}</span>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:dept:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:dept:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 添加或修改菜单对话框 -->
<XModal id="deptModel" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form ref="formRef" :schema="allSchemas.formSchema" :rules="rules">
<template #parentId="form">
<el-tree-select
node-key="id"
v-model="form['parentId']"
:props="defaultProps"
:data="deptOptions"
:default-expanded-keys="[100]"
check-strictly
/>
</template>
<template #leaderUserId="form">
<el-select v-model="form['leaderUserId']">
<el-option
v-for="item in userOption"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</template>
</Form>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm()"
:title="t('action.save')"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" @click="dialogVisible = false" :title="t('dialog.close')" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Dept">
import { handleTree, defaultProps } from '@/utils/tree'
import type { FormExpose } from '@/components/Form'
import { allSchemas, rules } from './dept.data'
import * as DeptApi from '@/api/system/dept'
import { getListSimpleUsersApi, UserVO } from '@/api/system/user'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const xGrid = ref<any>() // 列表 Grid Ref
const treeConfig = {
transform: true,
rowField: 'id',
parentField: 'parentId',
expandAll: true
}
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 遮罩层
const formRef = ref<FormExpose>() // 表单 Ref
const deptOptions = ref() // 树形结构
const userOption = ref<UserVO[]>([])
const getUserList = async () => {
const res = await getListSimpleUsersApi()
userOption.value = res
}
// 获取下拉框[上级]的数据
const getTree = async () => {
deptOptions.value = []
const res = await DeptApi.listSimpleDeptApi()
let dept: Tree = { id: 0, name: '顶级部门', children: [] }
dept.children = handleTree(res)
deptOptions.value.push(dept)
}
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
treeConfig: treeConfig,
getListApi: DeptApi.getDeptPageApi,
deleteApi: DeptApi.deleteDeptApi
})
// ========== 新增/修改 ==========
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = async () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await DeptApi.getDeptApi(rowId)
await nextTick()
unref(formRef)?.setValues(res)
}
// 提交新增/修改的表单
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as DeptApi.DeptVO
if (actionType.value === 'create') {
await DeptApi.createDeptApi(data)
message.success(t('common.createSuccess'))
} else if (actionType.value === 'update') {
await DeptApi.updateDeptApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
await getTree()
await reload()
}
}
})
}
const userNicknameFormat = (row) => {
if (!row || !row.leaderUserId) {
return '未设置'
}
for (const user of userOption.value) {
if (row.leaderUserId === user.id) {
return user.nickname
}
}
return '未知【' + row.leaderUserId + '】'
}
// ========== 初始化 ==========
onMounted(async () => {
await getUserList()
await getTree()
})
</script>

View File

@@ -0,0 +1,104 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const dictDataRules = reactive({
label: [required],
value: [required],
sort: [required]
})
// crudSchemas
export const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: null,
action: true,
actionWidth: '140px',
searchSpan: 12,
columns: [
{
title: '字典类型',
field: 'dictType',
isTable: false,
isForm: false
},
{
title: '数据标签',
field: 'label',
isSearch: true
},
{
title: '数据键值',
field: 'value'
},
// {
// title: '标签类型',
// field: 'colorType',
// form: {
// component: 'Select',
// componentProps: {
// options: [
// {
// label: 'default',
// value: ''
// },
// {
// label: 'success',
// value: 'success'
// },
// {
// label: 'info',
// value: 'info'
// },
// {
// label: 'warning',
// value: 'warning'
// },
// {
// label: 'danger',
// value: 'danger'
// }
// ]
// }
// },
// isTable: false
// },
{
title: '颜色',
field: 'cssClass',
isTable: false,
form: {
component: 'ColorPicker',
componentProps: {
predefine: ['#ffffff', '#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#c71585']
}
}
},
{
title: '显示排序',
field: 'sort',
isTable: false
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number'
},
{
title: t('form.remark'),
field: 'remark',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
},
isTable: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,65 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const dictTypeRules = reactive({
name: [required],
type: [required]
})
// 新增 + 修改
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: null,
action: true,
actionWidth: '140px',
searchSpan: 12,
columns: [
{
title: '字典名称',
field: 'name',
isSearch: true
},
{
title: '字典类型',
field: 'type',
isSearch: true
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
table: {
width: 70
}
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
isTable: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: t('form.remark'),
field: 'remark',
isTable: false,
form: {
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,251 @@
<template>
<div class="flex">
<!-- ====== 字典分类 ====== -->
<el-card class="w-1/2 dict" :gutter="12" shadow="always">
<template #header>
<div class="card-header">
<span>字典分类</span>
</div>
</template>
<XTable @register="registerType" @cell-click="cellClickEvent">
<!-- 操作新增类型 -->
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:dict:create']"
@click="handleTypeCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作编辑类型 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:dict:update']"
@click="handleTypeUpdate(row.id)"
/>
<!-- 操作删除类型 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:dict:delete']"
@click="typeDeleteData(row.id)"
/>
</template>
</XTable>
<!-- @星语分页和列表重叠在一起了 -->
</el-card>
<!-- ====== 字典数据 ====== -->
<el-card class="w-1/2 dict ml-3" :gutter="12" shadow="hover">
<template #header>
<div class="card-header">
<span>字典数据</span>
</div>
</template>
<!-- 列表 -->
<div v-if="!tableTypeSelect">
<span>请从左侧选择</span>
</div>
<div v-if="tableTypeSelect">
<!-- 列表 -->
<XTable @register="registerData">
<!-- 操作新增数据 -->
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:dict:create']"
@click="handleDataCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改数据 -->
<XTextButton
v-hasPermi="['system:dict:update']"
preIcon="ep:edit"
:title="t('action.edit')"
@click="handleDataUpdate(row.id)"
/>
<!-- 操作删除数据 -->
<XTextButton
v-hasPermi="['system:dict:delete']"
preIcon="ep:delete"
:title="t('action.del')"
@click="dataDeleteData(row.id)"
/>
</template>
</XTable>
</div>
</el-card>
<XModal id="dictModel" v-model="dialogVisible" :title="dialogTitle">
<Form
v-if="['typeCreate', 'typeUpdate'].includes(actionType)"
:schema="DictTypeSchemas.allSchemas.formSchema"
:rules="DictTypeSchemas.dictTypeRules"
ref="typeFormRef"
>
<template #type>
<template v-if="actionType == 'typeUpdate'">
<el-tag>{{ dictTypeValue }}</el-tag>
</template>
<template v-else><el-input v-model="dictTypeValue" /> </template>
</template>
</Form>
<Form
v-if="['dataCreate', 'dataUpdate'].includes(actionType)"
:schema="DictDataSchemas.allSchemas.formSchema"
:rules="DictDataSchemas.dictDataRules"
ref="dataFormRef"
/>
<!-- 操作按钮 -->
<template #footer>
<XButton
v-if="['typeCreate', 'typeUpdate'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitTypeForm"
/>
<XButton
v-if="['dataCreate', 'dataUpdate'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitDataForm"
/>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</div>
</template>
<script setup lang="ts" name="Dict">
import { VxeTableEvents } from 'vxe-table'
import type { FormExpose } from '@/components/Form'
import * as DictTypeSchemas from './dict.type'
import * as DictDataSchemas from './dict.data'
import * as DictTypeApi from '@/api/system/dict/dict.type'
import * as DictDataApi from '@/api/system/dict/dict.data'
import { DictDataVO, DictTypeVO } from '@/api/system/dict/types'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const [registerType, { reload: typeGetList, deleteData: typeDeleteData }] = useXTable({
allSchemas: DictTypeSchemas.allSchemas,
getListApi: DictTypeApi.getDictTypePageApi,
deleteApi: DictTypeApi.deleteDictTypeApi
})
const queryParams = reactive({
dictType: null
})
const [registerData, { reload: dataGetList, deleteData: dataDeleteData }] = useXTable({
allSchemas: DictDataSchemas.allSchemas,
params: queryParams,
getListApi: DictDataApi.getDictDataPageApi,
deleteApi: DictDataApi.deleteDictDataApi
})
// ========== 字典分类列表相关 ==========
const dictTypeValue = ref('')
// 字典分类修改操作
const handleTypeCreate = () => {
dictTypeValue.value = ''
setDialogTile('typeCreate')
}
const handleTypeUpdate = async (rowId: number) => {
setDialogTile('typeUpdate')
// 设置数据
const res = await DictTypeApi.getDictTypeApi(rowId)
dictTypeValue.value = res.type
unref(typeFormRef)?.setValues(res)
}
// 字典数据修改操作
const handleDataCreate = () => {
setDialogTile('dataCreate')
}
const handleDataUpdate = async (rowId: number) => {
setDialogTile('dataUpdate')
// 设置数据
const res = await DictDataApi.getDictDataApi(rowId)
unref(dataFormRef)?.setValues(res)
}
// 字典分类点击行事件
const parentType = ref('')
const tableTypeSelect = ref(false)
const cellClickEvent: VxeTableEvents.CellClick = async ({ row }) => {
tableTypeSelect.value = true
queryParams.dictType = row['type']
await dataGetList()
parentType.value = row['type']
}
// 弹出框
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionLoading = ref(false) // 遮罩层
const typeFormRef = ref<FormExpose>() // 分类表单 Ref
const dataFormRef = ref<FormExpose>() // 数据表单 Ref
const actionType = ref('') // 操作按钮的类型
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 提交按钮
const submitTypeForm = async () => {
const elForm = unref(typeFormRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid && dictTypeValue.value != '') {
actionLoading.value = true
// 提交请求
try {
const data = unref(typeFormRef)?.formModel as DictTypeVO
if (actionType.value === 'typeCreate') {
data.type = dictTypeValue.value
await DictTypeApi.createDictTypeApi(data)
message.success(t('common.createSuccess'))
} else if (actionType.value === 'typeUpdate') {
await DictTypeApi.updateDictTypeApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
typeGetList()
}
}
})
}
const submitDataForm = async () => {
const elForm = unref(dataFormRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(dataFormRef)?.formModel as DictDataVO
if (actionType.value === 'dataCreate') {
data.dictType = parentType.value
await DictDataApi.createDictDataApi(data)
message.success(t('common.createSuccess'))
} else if (actionType.value === 'dataUpdate') {
await DictDataApi.updateDictDataApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
dataGetList()
}
}
})
}
</script>

View File

@@ -0,0 +1,54 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
applicationName: [required],
code: [required],
message: [required]
})
// 新增 + 修改
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '编号',
action: true,
columns: [
{
title: '错误码类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_ERROR_CODE_TYPE,
dictClass: 'number',
isSearch: true
},
{
title: '应用名',
field: 'applicationName',
isSearch: true
},
{
title: '错误码编码',
field: 'code',
isSearch: true
},
{
title: '错误码错误提示',
field: 'message',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,145 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:error-code:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:error-code:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:error-code:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:error-code:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="errorCodeModel" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="ErrorCode">
import type { FormExpose } from '@/components/Form'
// 业务相关的 import
import { rules, allSchemas } from './errorCode.data'
import * as ErrorCodeApi from '@/api/system/errorCode'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: ErrorCodeApi.getErrorCodePageApi,
deleteApi: ErrorCodeApi.deleteErrorCodeApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await ErrorCodeApi.getErrorCodeApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
// 设置数据
const res = await ErrorCodeApi.getErrorCodeApi(rowId)
detailData.value = res
}
// 提交新增/修改的表单
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as ErrorCodeApi.ErrorCodeVO
if (actionType.value === 'create') {
await ErrorCodeApi.createErrorCodeApi(data)
message.success(t('common.createSuccess'))
} else {
await ErrorCodeApi.updateErrorCodeApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,53 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作导出 -->
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
@click="exportList('登录列表.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
</template>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="postModel" v-model="dialogVisible" :title="dialogTitle">
<!-- 表单详情 -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailData" />
<template #footer>
<!-- 按钮关闭 -->
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Loginlog">
// 业务相关的 import
import { allSchemas } from './loginLog.data'
import { getLoginLogPageApi, exportLoginLogApi, LoginLogVO } from '@/api/system/loginLog'
const { t } = useI18n() // 国际化
// 列表相关的变量
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: getLoginLogPageApi,
exportListApi: exportLoginLogApi
})
// 详情操作
const detailData = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref(t('action.detail')) // 弹出层标题
// 详情
const handleDetail = async (row: LoginLogVO) => {
// 设置数据
detailData.value = row
dialogVisible.value = true
}
</script>

View File

@@ -0,0 +1,53 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '日志编号',
action: true,
actionWidth: '100px',
columns: [
{
title: '日志类型',
field: 'logType',
dictType: DICT_TYPE.SYSTEM_LOGIN_TYPE,
dictClass: 'number'
},
{
title: '用户名称',
field: 'username',
isSearch: true
},
{
title: '登录地址',
field: 'userIp',
isSearch: true
},
{
title: '浏览器',
field: 'userAgent'
},
{
title: '登陆结果',
field: 'result',
dictType: DICT_TYPE.SYSTEM_LOGIN_RESULT,
dictClass: 'number'
},
{
title: '登录日期',
field: 'createTime',
formatter: 'formatDate',
table: {
width: 150
},
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,65 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 表单校验
export const rules = reactive({
mail: [required],
username: [required],
password: [required],
host: [required],
port: [required],
sslEnable: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id', // 默认的主键 ID
primaryTitle: '编号',
primaryType: 'id',
action: true,
actionWidth: '200', // 3 个按钮默认 200如有删减对应增减即可
columns: [
{
title: '邮箱',
field: 'mail',
isSearch: true
},
{
title: '用户名',
field: 'username',
isSearch: true
},
{
title: '密码',
field: 'password',
isTable: false
},
{
title: 'SMTP 服务器域名',
field: 'host'
},
{
title: 'SMTP 服务器端口',
field: 'port',
form: {
component: 'InputNumber',
value: 465
}
},
{
title: '是否开启 SSL',
field: 'sslEnable',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
dictClass: 'boolean'
},
{
title: '创建时间',
field: 'createTime',
isForm: false,
formatter: 'formatDate',
table: {
width: 180
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,151 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:mail-account:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:mail-account:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:mail-account:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:mail-account:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="mailAccountModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
<!-- 表单添加/修改 -->
<Form
ref="formRef"
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
/>
<!-- 表单详情 -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="MailAccount">
import { FormExpose } from '@/components/Form'
// 业务相关的 import
import { rules, allSchemas } from './account.data'
import * as MailAccountApi from '@/api/system/mail/account'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: MailAccountApi.getMailAccountPageApi,
deleteApi: MailAccountApi.deleteMailAccountApi
})
// 弹窗相关的变量
const modelVisible = ref(false) // 是否显示弹出层
const modelTitle = ref('edit') // 弹出层标题
const modelLoading = ref(false) // 弹出层loading
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
modelLoading.value = true
modelTitle.value = t('action.' + type)
actionType.value = type
modelVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
modelLoading.value = false
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await MailAccountApi.getMailAccountApi(rowId)
unref(formRef)?.setValues(res)
modelLoading.value = false
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await MailAccountApi.getMailAccountApi(rowId)
detailData.value = res
modelLoading.value = false
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as MailAccountApi.MailAccountVO
if (actionType.value === 'create') {
await MailAccountApi.createMailAccountApi(data)
message.success(t('common.createSuccess'))
} else {
await MailAccountApi.updateMailAccountApi(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,97 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #accountId_search>
<el-select v-model="queryParams.accountId">
<el-option :key="undefined" label="全部" :value="undefined" />
<el-option
v-for="item in accountOptions"
:key="item.id"
:label="item.mail"
:value="item.id"
/>
</el-select>
</template>
<template #toMail_default="{ row }">
<div>{{ row.toMail }}</div>
<div v-if="row.userType && row.userId">
<DictTag :type="DICT_TYPE.USER_TYPE" :value="row.userType" />{{ '(' + row.userId + ')' }}
</div>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:mail-log:query']"
@click="handleDetail(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="mailLogModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
<!-- 表单详情 -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<template #footer>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="MailLog">
// 业务相关的 import
import { DICT_TYPE } from '@/utils/dict'
import { allSchemas } from './log.data'
import * as MailLogApi from '@/api/system/mail/log'
import * as MailAccountApi from '@/api/system/mail/account'
const { t } = useI18n() // 国际化
// 列表相关的变量
const queryParams = reactive({
accountId: null
})
const [registerTable] = useXTable({
allSchemas: allSchemas,
params: queryParams,
getListApi: MailLogApi.getMailLogPageApi
})
const accountOptions = ref<any[]>([]) // 账号下拉选项
// 弹窗相关的变量
const modelVisible = ref(false) // 是否显示弹出层
const modelTitle = ref('edit') // 弹出层标题
const modelLoading = ref(false) // 弹出层loading
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
modelLoading.value = true
modelTitle.value = t('action.' + type)
actionType.value = type
modelVisible.value = true
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await MailLogApi.getMailLogApi(rowId)
detailData.value = res
modelLoading.value = false
}
// ========== 初始化 ==========
onMounted(() => {
MailAccountApi.getSimpleMailAccounts().then((data) => {
accountOptions.value = data
})
})
</script>

View File

@@ -0,0 +1,121 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryTitle: '编号',
primaryType: 'id',
action: true,
actionWidth: '70',
columns: [
{
title: '发送时间',
field: 'sendTime',
table: {
width: 180
},
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: '接收邮箱',
field: 'toMail',
isSearch: true,
table: {
width: 180,
slots: {
default: 'toMail_default'
}
}
},
{
title: '用户编号',
field: 'userId',
isSearch: true,
isTable: false
},
{
title: '用户类型',
field: 'userType',
dictType: DICT_TYPE.USER_TYPE,
dictClass: 'number',
isSearch: true,
isTable: false
},
{
title: '邮件标题',
field: 'templateTitle'
},
{
title: '邮件内容',
field: 'templateContent',
isTable: false
},
{
title: '邮箱参数',
field: 'templateParams',
isTable: false
},
{
title: '发送状态',
field: 'sendStatus',
dictType: DICT_TYPE.SYSTEM_MAIL_SEND_STATUS,
dictClass: 'string',
isSearch: true
},
{
title: '邮箱账号',
field: 'accountId',
isSearch: true,
isTable: false,
search: {
slots: {
default: 'accountId_search'
}
}
},
{
title: '发送邮箱地址',
field: 'fromMail',
table: {
title: '邮箱账号'
}
},
{
title: '模板编号',
field: 'templateId',
isSearch: true
},
{
title: '模板编码',
field: 'templateCode',
isTable: false
},
{
title: '模版发送人名称',
field: 'templateNickname',
isTable: false
},
{
title: '发送返回的消息编号',
field: 'sendMessageId',
isTable: false
},
{
title: '发送异常',
field: 'sendException',
isTable: false
},
{
title: '创建时间',
field: 'createTime',
isTable: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,273 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #accountId_search>
<el-select v-model="queryParams.accountId">
<el-option :key="undefined" label="全部" :value="undefined" />
<el-option
v-for="item in accountOptions"
:key="item.id"
:label="item.mail"
:value="item.id"
/>
</el-select>
</template>
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:mail-template:create']"
@click="handleCreate()"
/>
</template>
<template #accountId_default="{ row }">
<span>{{ accountOptions.find((account) => account.id === row.accountId)?.mail }}</span>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作测试短信 -->
<XTextButton
preIcon="ep:cpu"
:title="t('action.test')"
v-hasPermi="['system:mail-template:send-mail']"
@click="handleSendMail(row)"
/>
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:mail-template:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:mail-template:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:mail-template:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 添加/修改/详情的弹窗 -->
<XModal id="mailTemplateModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
<!-- 表单添加/修改 -->
<Form
ref="formRef"
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
>
<template #accountId="form">
<el-select v-model="form.accountId">
<el-option
v-for="item in accountOptions"
:key="item.id"
:label="item.mail"
:value="item.id"
/>
</el-select>
</template>
</Form>
<!-- 表单详情 -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />
</template>
</XModal>
<!-- 测试邮件的弹窗 -->
<XModal id="sendTest" v-model="sendVisible" title="测试">
<el-form :model="sendForm" :rules="sendRules" label-width="200px" label-position="top">
<el-form-item label="模板内容" prop="content">
<Editor :model-value="sendForm.content" readonly height="150px" />
</el-form-item>
<el-form-item label="收件邮箱" prop="mail">
<el-input v-model="sendForm.mail" placeholder="请输入收件邮箱" />
</el-form-item>
<el-form-item
v-for="param in sendForm.params"
:key="param"
:label="'参数 {' + param + '}'"
:prop="'templateParams.' + param"
>
<el-input
v-model="sendForm.templateParams[param]"
:placeholder="'请输入 ' + param + ' 参数'"
/>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<XButton
type="primary"
:title="t('action.test')"
:loading="actionLoading"
@click="sendTest()"
/>
<XButton :title="t('dialog.close')" @click="sendVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="MailTemplate">
import { FormExpose } from '@/components/Form'
// 业务相关的 import
import { rules, allSchemas } from './template.data'
import * as MailTemplateApi from '@/api/system/mail/template'
import * as MailAccountApi from '@/api/system/mail/account'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const queryParams = reactive({
accountId: null
})
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
params: queryParams,
getListApi: MailTemplateApi.getMailTemplatePageApi,
deleteApi: MailTemplateApi.deleteMailTemplateApi
})
const accountOptions = ref<any[]>([]) // 账号下拉选项
// 弹窗相关的变量
const modelVisible = ref(false) // 是否显示弹出层
const modelTitle = ref('edit') // 弹出层标题
const modelLoading = ref(false) // 弹出层loading
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
modelLoading.value = true
modelTitle.value = t('action.' + type)
actionType.value = type
modelVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
modelLoading.value = false
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await MailTemplateApi.getMailTemplateApi(rowId)
unref(formRef)?.setValues(res)
modelLoading.value = false
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await MailTemplateApi.getMailTemplateApi(rowId)
detailData.value = res
modelLoading.value = false
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as MailTemplateApi.MailTemplateVO
if (actionType.value === 'create') {
await MailTemplateApi.createMailTemplateApi(data)
message.success(t('common.createSuccess'))
} else {
await MailTemplateApi.updateMailTemplateApi(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
// ========== 测试相关 ==========
const sendForm = ref({
content: '',
params: {},
mail: '',
templateCode: '',
templateParams: {}
})
const sendRules = ref({
mail: [{ required: true, message: '邮箱不能为空', trigger: 'blur' }],
templateCode: [{ required: true, message: '模版编号不能为空', trigger: 'blur' }],
templateParams: {}
})
const sendVisible = ref(false)
const handleSendMail = (row: any) => {
sendForm.value.content = row.content
sendForm.value.params = row.params
sendForm.value.templateCode = row.code
sendForm.value.templateParams = row.params.reduce(function (obj, item) {
obj[item] = undefined
return obj
}, {})
sendRules.value.templateParams = row.params.reduce(function (obj, item) {
obj[item] = { required: true, message: '参数 ' + item + ' 不能为空', trigger: 'change' }
return obj
}, {})
sendVisible.value = true
}
const sendTest = async () => {
const data: MailTemplateApi.MailSendReqVO = {
mail: sendForm.value.mail,
templateCode: sendForm.value.templateCode,
templateParams: sendForm.value.templateParams as unknown as Map<string, Object>
}
const res = await MailTemplateApi.sendMailApi(data)
if (res) {
message.success('提交发送成功!发送结果,见发送日志编号:' + res)
}
sendVisible.value = false
}
// ========== 初始化 ==========
onMounted(() => {
MailAccountApi.getSimpleMailAccounts().then((data) => {
accountOptions.value = data
})
})
</script>

View File

@@ -0,0 +1,98 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
accountId: [required],
title: [required],
content: [required],
params: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id', // 默认的主键ID
primaryTitle: '编号', // 默认显示的值
primaryType: null,
action: true,
actionWidth: '260',
columns: [
{
title: '模板编码',
field: 'code',
isSearch: true
},
{
title: '模板名称',
field: 'name',
isSearch: true
},
{
title: '模板标题',
field: 'title'
},
{
title: '模板内容',
field: 'content',
form: {
component: 'Editor',
colProps: {
span: 24
},
componentProps: {
valueHtml: ''
}
}
},
{
title: '邮箱账号',
field: 'accountId',
isSearch: true,
table: {
width: 200,
slots: {
default: 'accountId_default'
}
},
search: {
slots: {
default: 'accountId_search'
}
}
},
{
title: '发送人名称',
field: 'nickname'
},
{
title: '开启状态',
field: 'status',
isSearch: true,
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number'
},
{
title: '备注',
field: 'remark',
isTable: false
},
{
title: '创建时间',
field: 'createTime',
isForm: false,
formatter: 'formatDate',
table: {
width: 180
},
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,351 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable ref="xGrid" @register="registerTable" show-overflow>
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:menu:create']"
@click="handleCreate()"
/>
<XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
<XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
</template>
<template #name_default="{ row }">
<Icon :icon="row.icon" />
<span class="ml-3">{{ row.name }}</span>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:menu:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:menu:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 添加或修改菜单对话框 -->
<XModal id="menuModel" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<el-form
ref="formRef"
:model="menuForm"
:rules="rules"
label-width="100px"
label-position="right"
>
<el-form-item label="上级菜单">
<el-tree-select
node-key="id"
v-model="menuForm.parentId"
:props="defaultProps"
:data="menuOptions"
:default-expanded-keys="[0]"
check-strictly
/>
</el-form-item>
<el-col :span="16">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="menuForm.name" placeholder="请输入菜单名称" clearable />
</el-form-item>
</el-col>
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="menuForm.type">
<el-radio-button
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)"
:key="dict.label"
:label="dict.value"
>
{{ dict.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="menuForm.type !== 3">
<el-form-item label="菜单图标">
<IconSelect v-model="menuForm.icon" clearable />
</el-form-item>
<el-col :span="16">
<el-form-item label="路由地址" prop="path">
<template #label>
<Tooltip
titel="路由地址"
message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
/>
</template>
<el-input v-model="menuForm.path" placeholder="请输入路由地址" clearable />
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type === 2">
<el-col :span="16">
<el-form-item label="组件地址" prop="component">
<el-input
v-model="menuForm.component"
placeholder="例如说system/user/index"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="组件名字" prop="componentName">
<el-input v-model="menuForm.componentName" placeholder="例如说SystemUser" clearable />
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type !== 1">
<el-col :span="16">
<el-form-item label="权限标识" prop="permission">
<template #label>
<Tooltip
titel="权限标识"
message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
/>
</template>
<el-input v-model="menuForm.permission" placeholder="请输入权限标识" clearable />
</el-form-item>
</el-col>
</template>
<el-col :span="16">
<el-form-item label="显示排序" prop="sort">
<el-input-number v-model="menuForm.sort" controls-position="right" :min="0" clearable />
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="菜单状态" prop="status">
<el-radio-group v-model="menuForm.status">
<el-radio
border
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.label"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<template v-if="menuForm.type !== 3">
<el-col :span="16">
<el-form-item label="显示状态" prop="visible">
<template #label>
<Tooltip
titel="显示状态"
message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问"
/>
</template>
<el-radio-group v-model="menuForm.visible">
<el-radio border key="true" :label="true">显示</el-radio>
<el-radio border key="false" :label="false">隐藏</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type !== 3">
<el-col :span="16">
<el-form-item label="总是显示" prop="alwaysShow">
<template #label>
<Tooltip
titel="总是显示"
message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
/>
</template>
<el-radio-group v-model="menuForm.alwaysShow">
<el-radio border key="true" :label="true">总是</el-radio>
<el-radio border key="false" :label="false">不是</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type === 2">
<el-col :span="16">
<el-form-item label="缓存状态" prop="keepAlive">
<template #label>
<Tooltip
titel="缓存状态"
message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
/>
</template>
<el-radio-group v-model="menuForm.keepAlive">
<el-radio border key="true" :label="true">缓存</el-radio>
<el-radio border key="false" :label="false">不缓存</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</template>
</el-form>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm()"
:title="t('action.save')"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" @click="dialogVisible = false" :title="t('dialog.close')" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Menu">
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { FormInstance } from 'element-plus'
// 业务相关的 import
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
import { handleTree, defaultProps } from '@/utils/tree'
import * as MenuApi from '@/api/system/menu'
import { allSchemas, rules } from './menu.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const { wsCache } = useCache()
const xGrid = ref<any>(null)
// 列表相关的变量
const treeConfig = {
transform: true,
rowField: 'id',
parentField: 'parentId',
expandAll: false
}
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
treeConfig: treeConfig,
getListApi: MenuApi.getMenuListApi,
deleteApi: MenuApi.deleteMenuApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 遮罩层
// 新增和修改的表单值
const formRef = ref<FormInstance>()
const menuForm = ref<MenuApi.MenuVO>({
id: 0,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
sort: 1,
parentId: 0,
path: '',
icon: '',
component: '',
componentName: '',
status: CommonStatusEnum.ENABLE,
visible: true,
keepAlive: true,
alwaysShow: true,
createTime: new Date()
})
// ========== 下拉框[上级菜单] ==========
const menuOptions = ref<any[]>([]) // 树形结构
// 获取下拉框[上级菜单]的数据
const getTree = async () => {
menuOptions.value = []
const res = await MenuApi.listSimpleMenusApi()
let menu: Tree = { id: 0, name: '主类目', children: [] }
menu.children = handleTree(res)
menuOptions.value.push(menu)
}
// ========== 新增/修改 ==========
// 设置标题
const setDialogTile = async (type: string) => {
await getTree()
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
formRef.value?.resetFields()
menuForm.value = {
id: 0,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
sort: 1,
parentId: 0,
path: '',
icon: '',
component: '',
componentName: '',
status: CommonStatusEnum.ENABLE,
visible: true,
keepAlive: true,
alwaysShow: true,
createTime: new Date()
}
}
// 修改操作
const handleUpdate = async (rowId: number) => {
await setDialogTile('update')
// 设置数据
const res = await MenuApi.getMenuApi(rowId)
menuForm.value = res
// TODO 芋艿:这块要优化下,部分字段未重置,无法修改
menuForm.value.componentName = res.componentName || ''
menuForm.value.alwaysShow = res.alwaysShow !== undefined ? res.alwaysShow : true
}
// 提交新增/修改的表单
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
if (
menuForm.value.type === SystemMenuTypeEnum.DIR ||
menuForm.value.type === SystemMenuTypeEnum.MENU
) {
if (!isExternal(menuForm.value.path)) {
if (menuForm.value.parentId === 0 && menuForm.value.path.charAt(0) !== '/') {
message.error('路径必须以 / 开头')
return
} else if (menuForm.value.parentId !== 0 && menuForm.value.path.charAt(0) === '/') {
message.error('路径不能以 / 开头')
return
}
}
}
if (actionType.value === 'create') {
await MenuApi.createMenuApi(menuForm.value)
message.success(t('common.createSuccess'))
} else {
await MenuApi.updateMenuApi(menuForm.value)
message.success(t('common.updateSuccess'))
}
} finally {
dialogVisible.value = false
actionLoading.value = false
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
// 操作成功,重新加载列表
await reload()
}
}
// 判断 path 是不是外部的 HTTP 等链接
const isExternal = (path: string) => {
return /^(https?:|mailto:|tel:)/.test(path)
}
</script>

View File

@@ -0,0 +1,76 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 新增和修改的表单校验
export const rules = reactive({
name: [required],
sort: [required],
path: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: null,
action: true,
columns: [
{
title: '上级菜单',
field: 'parentId',
isTable: false
},
{
title: '菜单名称',
field: 'name',
isSearch: true,
table: {
treeNode: true,
align: 'left',
width: '200px',
slots: {
default: 'name_default'
}
}
},
{
title: '菜单类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_MENU_TYPE
},
{
title: '路由地址',
field: 'path'
},
{
title: '组件路径',
field: 'component'
},
{
title: '组件名字',
field: 'componentName'
},
{
title: '权限标识',
field: 'permission'
},
{
title: '排序',
field: 'sort'
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isTable: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,148 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:notice:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:notice:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:notice:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:notice:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="noticeModel" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
ref="formRef"
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
>
<template #content="{ row }">
<Editor :model-value="row.content" :readonly="true" />
</template>
</Descriptions>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Notice">
import type { FormExpose } from '@/components/Form'
// 业务相关的 import
import * as NoticeApi from '@/api/system/notice'
import { rules, allSchemas } from './notice.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: NoticeApi.getNoticePageApi,
deleteApi: NoticeApi.deleteNoticeApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await NoticeApi.getNoticeApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
// 设置数据
const res = await NoticeApi.getNoticeApi(rowId)
detailData.value = res
}
// 提交新增/修改的表单
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as NoticeApi.NoticeVO
if (actionType.value === 'create') {
await NoticeApi.createNoticeApi(data)
message.success(t('common.createSuccess'))
} else {
await NoticeApi.updateNoticeApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,59 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
title: [required],
type: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
action: true,
columns: [
{
title: '公告标题',
field: 'title',
isSearch: true
},
{
title: '公告类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_NOTICE_TYPE,
dictClass: 'number'
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '公告内容',
field: 'content',
table: {
type: 'html'
},
form: {
component: 'Editor',
colProps: {
span: 24
},
componentProps: {
valueHtml: ''
}
},
isTable: false
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,66 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:notify-message:query']"
@click="handleDetail(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="messageModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
<!-- 表单详情 -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<template #footer>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="NotifyMessage">
// 业务相关的 import
import { allSchemas } from './message.data'
import * as NotifyMessageApi from '@/api/system/notify/message'
const { t } = useI18n() // 国际化
// 列表相关的变量
const [registerTable] = useXTable({
allSchemas: allSchemas,
getListApi: NotifyMessageApi.getNotifyMessagePageApi
})
// 弹窗相关的变量
const modelVisible = ref(false) // 是否显示弹出层
const modelTitle = ref('edit') // 弹出层标题
const modelLoading = ref(false) // 弹出层loading
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
modelLoading.value = true
modelTitle.value = t('action.' + type)
actionType.value = type
modelVisible.value = true
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await NotifyMessageApi.getNotifyMessageApi(rowId)
detailData.value = res
modelLoading.value = false
}
</script>

View File

@@ -0,0 +1,101 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id', // 默认的主键ID
primaryTitle: '编号', // 默认显示的值
primaryType: 'seq', // 默认为seq序号模式
action: true,
actionWidth: '200', // 3个按钮默认200如有删减对应增减即可
columns: [
{
title: '用户编号',
field: 'userId',
isSearch: true
},
{
title: '用户类型',
field: 'userType',
dictType: DICT_TYPE.USER_TYPE,
dictClass: 'string',
isSearch: true,
table: {
width: 80
}
},
{
title: '模版编号',
field: 'templateId'
},
{
title: '模板编码',
field: 'templateCode',
isSearch: true,
table: {
width: 80
}
},
{
title: '发送人名称',
field: 'templateNickname',
table: {
width: 120
}
},
{
title: '模版内容',
field: 'templateContent',
table: {
width: 200
}
},
{
title: '模版类型',
field: 'templateType',
dictType: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
dictClass: 'number',
isSearch: true,
table: {
width: 80
}
},
{
title: '模版参数',
field: 'templateParams',
isTable: false
},
{
title: '是否已读',
field: 'readStatus',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
dictClass: 'boolean',
table: {
width: 80
}
},
{
title: '阅读时间',
field: 'readTime',
formatter: 'formatDate',
table: {
width: 180
}
},
{
title: '创建时间',
field: 'createTime',
isForm: false,
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
},
table: {
width: 180
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,58 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作标记已读 -->
<XButton type="primary" preIcon="ep:zoom-in" title="标记已读" @click="handleUpdateList" />
<!-- 操作全部已读 -->
<XButton type="primary" preIcon="ep:zoom-in" title="全部已读" @click="handleUpdateAll" />
</template>
<template #actionbtns_default="{ row }">
<!-- 操作已读 -->
<XTextButton
preIcon="ep:view"
title="已读"
v-hasPermi="['system:notify-message:query']"
v-if="!row.readStatus"
@click="handleUpdate([row.id])"
/>
</template>
</XTable>
</ContentWrap>
</template>
<script setup lang="ts" name="MyNotifyMessage">
// 业务相关的 import
import { allSchemas } from './my.data'
import * as NotifyMessageApi from '@/api/system/notify/message'
const message = useMessage() // 消息
// 列表相关的变量
const [registerTable, { reload, getCheckboxRecords }] = useXTable({
allSchemas: allSchemas,
getListApi: NotifyMessageApi.getMyNotifyMessagePage
})
const handleUpdateList = async () => {
const list = getCheckboxRecords()
if (list.length === 0) {
return
}
await handleUpdate(list.map((v) => v.id))
}
// 标记指定 id 已读
const handleUpdate = async (ids) => {
await NotifyMessageApi.updateNotifyMessageRead(ids)
message.success('标记已读成功!')
reload()
}
// 标记全部已读
const handleUpdateAll = async () => {
await NotifyMessageApi.updateAllNotifyMessageRead()
message.success('全部已读成功!')
reload()
}
</script>

View File

@@ -0,0 +1,58 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryTitle: ' ',
primaryType: 'checkbox',
action: true,
actionWidth: '200', // 3个按钮默认200如有删减对应增减即可
columns: [
{
title: '发送人名称',
field: 'templateNickname',
table: {
width: 120
}
},
{
title: '发送时间',
field: 'createTime',
isForm: false,
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
},
table: {
width: 180
}
},
{
title: '类型',
field: 'templateType',
dictType: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
dictClass: 'number',
table: {
width: 80
}
},
{
title: '内容',
field: 'templateContent'
},
{
title: '是否已读',
field: 'readStatus',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
dictClass: 'boolean',
table: {
width: 80
},
isSearch: true
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,251 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:notify-template:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作测试站内信 -->
<XTextButton
preIcon="ep:cpu"
:title="t('action.test')"
v-hasPermi="['system:notify-template:send-notify']"
@click="handleSendNotify(row)"
/>
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:notify-template:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:notify-template:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:notify-template:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 添加/修改的弹窗 -->
<XModal id="templateModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
<!-- 表单添加/修改 -->
<Form
ref="formRef"
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
/>
<!-- 表单详情 -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />
</template>
</XModal>
<!-- 测试站内信的弹窗 -->
<XModal id="sendTest" v-model="sendVisible" title="测试">
<el-form :model="sendForm" :rules="sendRules" label-width="200px" label-position="top">
<el-form-item label="模板内容" prop="content">
<el-input type="textarea" v-model="sendForm.content" readonly />
</el-form-item>
<el-form-item label="接收人" prop="userId">
<el-select v-model="sendForm.userId" placeholder="请选择接收人">
<el-option
v-for="item in userOption"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-for="param in sendForm.params"
:key="param"
:label="'参数 {' + param + '}'"
:prop="'templateParams.' + param"
>
<el-input
v-model="sendForm.templateParams[param]"
:placeholder="'请输入 ' + param + ' 参数'"
/>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<XButton
type="primary"
:title="t('action.test')"
:loading="actionLoading"
@click="sendTest()"
/>
<XButton :title="t('dialog.close')" @click="sendVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="NotifyTemplate">
import { FormExpose } from '@/components/Form'
// 业务相关的 import
import { rules, allSchemas } from './template.data'
import * as NotifyTemplateApi from '@/api/system/notify/template'
import { getListSimpleUsersApi, UserVO } from '@/api/system/user'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: NotifyTemplateApi.getNotifyTemplatePageApi,
deleteApi: NotifyTemplateApi.deleteNotifyTemplateApi
})
// 弹窗相关的变量
const modelVisible = ref(false) // 是否显示弹出层
const modelTitle = ref('edit') // 弹出层标题
const modelLoading = ref(false) // 弹出层loading
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
modelLoading.value = true
modelTitle.value = t('action.' + type)
actionType.value = type
modelVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
modelLoading.value = false
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await NotifyTemplateApi.getNotifyTemplateApi(rowId)
unref(formRef)?.setValues(res)
modelLoading.value = false
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await NotifyTemplateApi.getNotifyTemplateApi(rowId)
detailData.value = res
modelLoading.value = false
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as NotifyTemplateApi.NotifyTemplateVO
if (actionType.value === 'create') {
await NotifyTemplateApi.createNotifyTemplateApi(data)
message.success(t('common.createSuccess'))
} else {
await NotifyTemplateApi.updateNotifyTemplateApi(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
// ========== 测试相关 ==========
const sendForm = ref({
content: '',
params: {},
userId: 0,
templateCode: '',
templateParams: {}
})
const sendRules = ref({
userId: [{ required: true, message: '用户编号不能为空', trigger: 'blur' }],
templateCode: [{ required: true, message: '模版编号不能为空', trigger: 'blur' }],
templateParams: {}
})
const sendVisible = ref(false)
const userOption = ref<UserVO[]>([])
const handleSendNotify = (row: any) => {
sendForm.value.content = row.content
sendForm.value.params = row.params
sendForm.value.templateCode = row.code
sendForm.value.templateParams = row.params.reduce(function (obj, item) {
obj[item] = undefined
return obj
}, {})
sendRules.value.templateParams = row.params.reduce(function (obj, item) {
obj[item] = { required: true, message: '参数 ' + item + ' 不能为空', trigger: 'change' }
return obj
}, {})
sendVisible.value = true
}
const sendTest = async () => {
const data: NotifyTemplateApi.NotifySendReqVO = {
userId: sendForm.value.userId,
templateCode: sendForm.value.templateCode,
templateParams: sendForm.value.templateParams as unknown as Map<string, Object>
}
const res = await NotifyTemplateApi.sendNotifyApi(data)
if (res) {
message.success('提交发送成功!发送结果,见发送日志编号:' + res)
}
sendVisible.value = false
}
// ========== 初始化 ==========
onMounted(() => {
getListSimpleUsersApi().then((data) => {
userOption.value = data
})
})
</script>

View File

@@ -0,0 +1,85 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
content: [required],
type: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryTitle: '编号',
primaryType: null,
action: true,
actionWidth: '260', // 3个按钮默认200如有删减对应增减即可
columns: [
{
title: '模版编码',
field: 'code',
isSearch: true
},
{
title: '模板名称',
field: 'name',
isSearch: true
},
{
title: '发件人名称',
field: 'nickname'
},
{
title: '类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
dictClass: 'number'
},
{
title: '模版内容',
field: 'content',
table: {
width: 300
},
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: '状态',
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '备注',
field: 'remark'
},
{
title: '创建时间',
field: 'createTime',
isForm: false,
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
},
table: {
width: 180
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,193 @@
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
const authorizedGrantOptions = getStrDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)
// 表单校验
export const rules = reactive({
clientId: [required],
secret: [required],
name: [required],
status: [required],
accessTokenValiditySeconds: [required],
refreshTokenValiditySeconds: [required],
redirectUris: [required],
authorizedGrantTypes: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'clientId',
primaryType: null,
action: true,
columns: [
{
title: '客户端密钥',
field: 'secret'
},
{
title: '应用名',
field: 'name',
isSearch: true
},
{
title: '应用图标',
field: 'logo',
table: {
cellRender: {
name: 'XImg'
}
},
form: {
component: 'UploadImg'
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '访问令牌的有效期',
field: 'accessTokenValiditySeconds',
form: {
component: 'InputNumber'
},
table: {
slots: {
default: 'accessTokenValiditySeconds_default'
}
}
},
{
title: '刷新令牌的有效期',
field: 'refreshTokenValiditySeconds',
form: {
component: 'InputNumber'
},
table: {
slots: {
default: 'refreshTokenValiditySeconds_default'
}
}
},
{
title: '授权类型',
field: 'authorizedGrantTypes',
table: {
width: 400,
slots: {
default: 'authorizedGrantTypes_default'
}
},
form: {
component: 'Select',
componentProps: {
options: authorizedGrantOptions,
multiple: true,
filterable: true
}
}
},
{
title: '授权范围',
field: 'scopes',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '自动授权范围',
field: 'autoApproveScopes',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '可重定向的 URI 地址',
field: 'redirectUris',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '权限',
field: 'authorities',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '资源',
field: 'resourceIds',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '附加信息',
field: 'additionalInformation',
isTable: false,
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,207 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:oauth2-client:create']"
@click="handleCreate()"
/>
</template>
<template #accessTokenValiditySeconds_default="{ row }">
{{ row.accessTokenValiditySeconds + '秒' }}
</template>
<template #refreshTokenValiditySeconds_default="{ row }">
{{ row.refreshTokenValiditySeconds + '秒' }}
</template>
<template #authorizedGrantTypes_default="{ row }">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(authorizedGrantType, index) in row.authorizedGrantTypes"
:index="index"
>
{{ authorizedGrantType }}
</el-tag>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:oauth2-client:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:oauth2-client:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:oauth2-client:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="postModel" v-model="dialogVisible" :title="dialogTitle">
<!-- 表单添加/修改 -->
<Form
ref="formRef"
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
/>
<!-- 表单详情 -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
>
<template #accessTokenValiditySeconds="{ row }">
{{ row.accessTokenValiditySeconds + '秒' }}
</template>
<template #refreshTokenValiditySeconds="{ row }">
{{ row.refreshTokenValiditySeconds + '秒' }}
</template>
<template #authorizedGrantTypes="{ row }">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(authorizedGrantType, index) in row.authorizedGrantTypes"
:index="index"
>
{{ authorizedGrantType }}
</el-tag>
</template>
<template #scopes="{ row }">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(scopes, index) in row.scopes"
:index="index"
>
{{ scopes }}
</el-tag>
</template>
<template #autoApproveScopes="{ row }">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(autoApproveScopes, index) in row.autoApproveScopes"
:index="index"
>
{{ autoApproveScopes }}
</el-tag>
</template>
<template #redirectUris="{ row }">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(redirectUris, index) in row.redirectUris"
:index="index"
>
{{ redirectUris }}
</el-tag>
</template>
</Descriptions>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Client">
import type { FormExpose } from '@/components/Form'
// 业务相关的 import
import * as ClientApi from '@/api/system/oauth2/client'
import { rules, allSchemas } from './client.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: ClientApi.getOAuth2ClientPageApi,
deleteApi: ClientApi.deleteOAuth2ClientApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await ClientApi.getOAuth2ClientApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await ClientApi.getOAuth2ClientApi(rowId)
detailData.value = res
}
// 提交新增/修改的表单
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as ClientApi.OAuth2ClientVO
if (actionType.value === 'create') {
await ClientApi.createOAuth2ClientApi(data)
message.success(t('common.createSuccess'))
} else {
await ClientApi.updateOAuth2ClientApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,64 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
<!-- 操作登出 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.logout')"
v-hasPermi="['system:oauth2-token:delete']"
@click="handleForceLogout(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailData" />
<!-- 操作按钮 -->
<template #footer>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Token">
import { allSchemas } from './token.data'
import * as TokenApi from '@/api/system/oauth2/token'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload }] = useXTable({
allSchemas: allSchemas,
topActionSlots: false,
getListApi: TokenApi.getAccessTokenPageApi
})
// ========== 详情相关 ==========
const detailData = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref(t('action.detail')) // 弹出层标题
// 详情
const handleDetail = async (row: TokenApi.OAuth2TokenVO) => {
// 设置数据
detailData.value = row
dialogVisible.value = true
}
// 强退操作
const handleForceLogout = (rowId: number) => {
message
.confirm('是否要强制退出用户')
.then(async () => {
await TokenApi.deleteAccessTokenApi(rowId)
message.success(t('common.success'))
})
.finally(async () => {
// 刷新列表
await reload()
})
}
</script>

View File

@@ -0,0 +1,48 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: null,
action: true,
columns: [
{
title: '用户编号',
field: 'userId',
isSearch: true
},
{
title: '访问令牌',
field: 'accessToken'
},
{
title: '刷新令牌',
field: 'refreshToken'
},
{
title: '客户端编号',
field: 'clientId',
isSearch: true
},
{
title: '用户类型',
field: 'userType',
dictType: DICT_TYPE.USER_TYPE,
dictClass: 'number',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false
},
{
title: '过期时间',
field: 'expiresTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,67 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:operate-log:export']"
@click="exportList('操作日志.xls')"
/>
</template>
<template #duration="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
<template #resultCode="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败' }}</span>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
</template>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="postModel" v-model="dialogVisible" :title="t('action.detail')">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailData">
<template #resultCode="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败' }}</span>
</template>
<template #duration="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
</Descriptions>
<template #footer>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="OperateLog">
// 业务相关的 import
import * as OperateLogApi from '@/api/system/operatelog'
import { allSchemas } from './operatelog.data'
const { t } = useI18n() // 国际化
// 列表相关的变量
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: OperateLogApi.getOperateLogPageApi,
exportListApi: OperateLogApi.exportOperateLogApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const actionLoading = ref(false) // 按钮 Loading
const detailData = ref() // 详情 Ref
// 详情
const handleDetail = (row: OperateLogApi.OperateLogVO) => {
// 设置数据
detailData.value = row
dialogVisible.value = true
}
</script>

View File

@@ -0,0 +1,106 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '日志编号',
action: true,
actionWidth: '80px',
columns: [
{
title: '操作模块',
field: 'module',
isSearch: true
},
{
title: '操作名',
field: 'name'
},
{
title: '操作类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_OPERATE_TYPE,
dictClass: 'number',
isSearch: true
},
{
title: '请求方法名',
field: 'requestMethod',
isTable: false
},
{
title: '请求地址',
field: 'requestUrl',
isTable: false
},
{
title: '操作人员',
field: 'userNickname',
isSearch: true
},
{
title: '操作明细',
field: 'content',
isTable: false
},
{
title: '用户 IP',
field: 'userIp',
isTable: false
},
{
title: 'userAgent',
field: 'userAgent'
},
{
title: '操作结果',
field: 'resultCode',
table: {
slots: {
default: 'resultCode'
}
}
},
{
title: '操作结果',
field: 'success',
isTable: false,
isDetail: false,
search: {
show: true,
itemRender: {
name: '$select',
props: { placeholder: t('common.selectText') },
options: [
{ label: '成功', value: 'true' },
{ label: '失败', value: 'false' }
]
}
}
},
{
title: '操作日期',
field: 'startTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: '执行时长',
field: 'duration',
table: {
slots: {
default: 'duration'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,92 @@
<template>
<!-- 弹窗 -->
<XModal id="PostForm" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
<!-- 表单添加/修改 -->
<Form
ref="formRef"
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
/>
<!-- 表单详情 -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts">
import type { FormExpose } from '@/components/Form'
import * as PostApi from '@/api/system/post'
import { rules, allSchemas } from './post.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const emit = defineEmits(['success'])
// 弹窗相关的变量
const modelVisible = ref(false) // 是否显示弹出层
const modelTitle = ref('update') // 弹出层标题
const modelLoading = ref(false) // 弹出层loading
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
const openModal = async (type: string, rowId?: number) => {
modelLoading.value = true
modelTitle.value = t('action.' + type)
actionType.value = type
modelVisible.value = true
// 设置数据
if (rowId) {
const res = await PostApi.getPostApi(rowId)
if (type === 'update') {
unref(formRef)?.setValues(res)
} else if (type === 'detail') {
detailData.value = res
}
}
modelLoading.value = false
}
// 提交新增/修改的表单
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
const valid = await elForm.validate()
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as PostApi.PostVO
if (actionType.value === 'create') {
await PostApi.createPostApi(data)
message.success(t('common.createSuccess'))
} else {
await PostApi.updatePostApi(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
emit('success')
} finally {
actionLoading.value = false
}
}
}
defineExpose({ openModal: openModal })
</script>

View File

@@ -0,0 +1,67 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:post:create']"
@click="openModal('create')"
/>
<!-- 操作导出 -->
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:post:export']"
@click="exportList('岗位列表.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
v-hasPermi="['system:post:update']"
@click="openModal('update', row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
v-hasPermi="['system:post:query']"
@click="openModal('detail', row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
v-hasPermi="['system:post:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<PostForm ref="modalRef" @success="reload()" />
</template>
<script setup lang="ts" name="Post">
// 业务相关的 import
import * as PostApi from '@/api/system/post'
import { allSchemas } from './post.data'
import PostForm from './PostForm.vue'
const { t } = useI18n() // 国际化
// 列表相关的变量
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: PostApi.getPostPageApi,
deleteApi: PostApi.deletePostApi,
exportListApi: PostApi.exportPostApi
})
// 表单相关的变量
const modalRef = ref()
const openModal = (type: string, rowId?: number) => {
modalRef.value.openModal(type, rowId)
}
</script>

View File

@@ -0,0 +1,52 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
sort: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '岗位编号',
action: true,
columns: [
{
title: '岗位名称',
field: 'name',
isSearch: true
},
{
title: '岗位编码',
field: 'code',
isSearch: true
},
{
title: '岗位顺序',
field: 'sort'
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '备注',
field: 'remark',
isTable: false
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,314 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:role:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作编辑 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:role:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:role:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作菜单权限 -->
<XTextButton
preIcon="ep:basketball"
title="菜单权限"
v-hasPermi="['system:permission:assign-role-menu']"
@click="handleScope('menu', row)"
/>
<!-- 操作数据权限 -->
<XTextButton
preIcon="ep:coin"
title="数据权限"
v-hasPermi="['system:permission:assign-role-data-scope']"
@click="handleScope('data', row)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:role:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
<XModal v-model="dialogScopeVisible" :title="dialogScopeTitle">
<el-form :model="dataScopeForm" label-width="140px" :inline="true">
<el-form-item label="角色名称">
<el-tag>{{ dataScopeForm.name }}</el-tag>
</el-form-item>
<el-form-item label="角色标识">
<el-tag>{{ dataScopeForm.code }}</el-tag>
</el-form-item>
<!-- 分配角色的数据权限对话框 -->
<el-form-item label="权限范围" v-if="actionScopeType === 'data'">
<el-select v-model="dataScopeForm.dataScope">
<el-option
v-for="item in dataScopeDictDatas"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- 分配角色的菜单权限对话框 -->
<el-form-item
label="权限范围"
v-if="
actionScopeType === 'menu' || dataScopeForm.dataScope === SystemDataScopeEnum.DEPT_CUSTOM
"
>
<el-card shadow="never">
<template #header>
父子联动(选中父节点,自动选择子节点):
<el-switch v-model="checkStrictly" inline-prompt active-text="是" inactive-text="否" />
全选/全不选:
<el-switch
v-model="treeNodeAll"
inline-prompt
active-text="是"
inactive-text="否"
@change="handleCheckedTreeNodeAll()"
/>
</template>
<el-tree
ref="treeRef"
node-key="id"
show-checkbox
:default-checked-keys="defaultCheckedKeys"
:check-strictly="!checkStrictly"
:props="defaultProps"
:data="treeOptions"
empty-text="加载中,请稍后"
/>
</el-card>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<XButton
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitScope()"
/>
<XButton
:loading="actionLoading"
:title="t('dialog.close')"
@click="dialogScopeVisible = false"
/>
</template>
</XModal>
</template>
<script setup lang="ts" name="Role">
import type { ElTree } from 'element-plus'
import type { FormExpose } from '@/components/Form'
import { handleTree, defaultProps } from '@/utils/tree'
import { SystemDataScopeEnum } from '@/utils/constants'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { rules, allSchemas } from './role.data'
import * as RoleApi from '@/api/system/role'
import { listSimpleMenusApi } from '@/api/system/menu'
import { listSimpleDeptApi } from '@/api/system/dept'
import * as PermissionApi from '@/api/system/permission'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: RoleApi.getRolePageApi,
deleteApi: RoleApi.deleteRoleApi
})
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await RoleApi.getRoleApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
// 设置数据
const res = await RoleApi.getRoleApi(rowId)
detailData.value = res
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as RoleApi.RoleVO
if (actionType.value === 'create') {
await RoleApi.createRoleApi(data)
message.success(t('common.createSuccess'))
} else {
await RoleApi.updateRoleApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
// ========== 数据权限 ==========
const dataScopeForm = reactive({
id: 0,
name: '',
code: '',
dataScope: 0,
checkList: []
})
const treeOptions = ref<any[]>([]) // 菜单树形结构
const treeRef = ref<InstanceType<typeof ElTree>>()
const dialogScopeVisible = ref(false)
const dialogScopeTitle = ref('数据权限')
const actionScopeType = ref('')
const dataScopeDictDatas = ref()
const defaultCheckedKeys = ref()
// 选项
const checkStrictly = ref(true)
const treeNodeAll = ref(false)
// 全选/全不选
const handleCheckedTreeNodeAll = () => {
treeRef.value!.setCheckedNodes(treeNodeAll.value ? treeOptions.value : [])
}
// 权限操作
const handleScope = async (type: string, row: RoleApi.RoleVO) => {
dataScopeForm.id = row.id
dataScopeForm.name = row.name
dataScopeForm.code = row.code
if (type === 'menu') {
const menuRes = await listSimpleMenusApi()
treeOptions.value = handleTree(menuRes)
const role = await PermissionApi.listRoleMenusApi(row.id)
if (role) {
// treeRef.value!.setCheckedKeys(role as unknown as Array<number>)
defaultCheckedKeys.value = role
}
} else if (type === 'data') {
const deptRes = await listSimpleDeptApi()
treeOptions.value = handleTree(deptRes)
const role = await RoleApi.getRoleApi(row.id)
dataScopeForm.dataScope = role.dataScope
if (role.dataScopeDeptIds) {
// treeRef.value!.setCheckedKeys(role.dataScopeDeptIds as unknown as Array<number>, false)
defaultCheckedKeys.value = role.dataScopeDeptIds
}
}
actionScopeType.value = type
dialogScopeVisible.value = true
}
// 保存权限
const submitScope = async () => {
if ('data' === actionScopeType.value) {
const data = ref<PermissionApi.PermissionAssignRoleDataScopeReqVO>({
roleId: dataScopeForm.id,
dataScope: dataScopeForm.dataScope,
dataScopeDeptIds:
dataScopeForm.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
? []
: (treeRef.value!.getCheckedKeys(false) as unknown as Array<number>)
})
await PermissionApi.assignRoleDataScopeApi(data.value)
} else if ('menu' === actionScopeType.value) {
const data = ref<PermissionApi.PermissionAssignRoleMenuReqVO>({
roleId: dataScopeForm.id,
menuIds: [
...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>),
...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>)
]
})
await PermissionApi.assignRoleMenuApi(data.value)
}
message.success(t('common.updateSuccess'))
dialogScopeVisible.value = false
}
const init = () => {
dataScopeDictDatas.value = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)
}
// ========== 初始化 ==========
onMounted(() => {
init()
})
</script>

View File

@@ -0,0 +1,75 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
sort: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryTitle: '角色编号',
primaryType: 'seq',
action: true,
actionWidth: '400px',
columns: [
{
title: '角色名称',
field: 'name',
isSearch: true
},
{
title: '角色类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_ROLE_TYPE,
dictClass: 'number',
isForm: false
},
{
title: '角色标识',
field: 'code',
isSearch: true
},
{
title: '显示顺序',
field: 'sort'
},
{
title: t('form.remark'),
field: 'remark',
isTable: false,
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,191 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:sensitive-word:create']"
@click="handleCreate()"
/>
<!-- 操作导出 -->
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:sensitive-word:export']"
@click="exportList('敏感词数据.xls')"
/>
</template>
<template #tags_default="{ row }">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(tag, index) in row.tags"
:index="index"
>
{{ tag }}
</el-tag>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:sensitive-word:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:sensitive-word:update']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:sensitive-word:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
>
<template #tags="form">
<el-select v-model="form['tags']" multiple placeholder="请选择">
<el-option v-for="item in tagsOptions" :key="item" :label="item" :value="item" />
</el-select>
</template>
</Form>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
>
<template #tags="{ row }">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(tag, index) in row.tags"
:index="index"
>
{{ tag }}
</el-tag>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="SensitiveWord">
import type { FormExpose } from '@/components/Form'
import * as SensitiveWordApi from '@/api/system/sensitiveWord'
import { rules, allSchemas } from './sensitiveWord.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: SensitiveWordApi.getSensitiveWordPageApi,
deleteApi: SensitiveWordApi.deleteSensitiveWordApi,
exportListApi: SensitiveWordApi.exportSensitiveWordApi
})
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 获取标签
const tagsOptions = ref()
const getTags = async () => {
const res = await SensitiveWordApi.getSensitiveWordTagsApi()
tagsOptions.value = res
}
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await SensitiveWordApi.getSensitiveWordApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await SensitiveWordApi.getSensitiveWordApi(rowId)
detailData.value = res
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as SensitiveWordApi.SensitiveWordVO
if (actionType.value === 'create') {
await SensitiveWordApi.createSensitiveWordApi(data)
message.success(t('common.createSuccess'))
} else {
await SensitiveWordApi.updateSensitiveWordApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
// ========== 初始化 ==========
onMounted(async () => {
await getTags()
})
</script>

View File

@@ -0,0 +1,74 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
tags: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '敏感词编号',
action: true,
columns: [
{
title: '敏感词',
field: 'name',
isSearch: true
},
{
title: '标签',
field: 'tag',
isTable: false,
isForm: false,
isDetail: false,
isSearch: true
},
{
title: '标签',
field: 'tags',
table: {
slots: {
default: 'tags_default'
}
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '描述',
field: 'description',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,147 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:sms-channel:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:sms-channel:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:sms-channel:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:sms-channel:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal id="smsChannel" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="SmsChannel">
import type { FormExpose } from '@/components/Form'
// 业务相关的 import
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
import { rules, allSchemas } from './sms.channel.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: SmsChannelApi.getSmsChannelPageApi,
deleteApi: SmsChannelApi.deleteSmsChannelApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await SmsChannelApi.getSmsChannelApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
const res = await SmsChannelApi.getSmsChannelApi(rowId)
detailData.value = res
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as SmsChannelApi.SmsChannelVO
if (actionType.value === 'create') {
await SmsChannelApi.createSmsChannelApi(data)
message.success(t('common.createSuccess'))
} else {
await SmsChannelApi.updateSmsChannelApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,63 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
signature: [required],
code: [required],
apiKey: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '渠道编号',
action: true,
columns: [
{
title: '短信签名',
field: 'signature',
isSearch: true
},
{
title: '渠道编码',
field: 'code',
dictType: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE,
isSearch: true
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '短信 API 的账号',
field: 'apiKey'
},
{
title: '短信 API 的密钥',
field: 'apiSecret'
},
{
title: '短信发送回调 URL',
field: 'callbackUrl'
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,56 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作导出 -->
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
@click="exportList('短信日志.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
</template>
</XTable>
</ContentWrap>
<XModal id="smsLog" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="SmsLog">
import { allSchemas } from './sms.log.data'
import * as SmsLoglApi from '@/api/system/sms/smsLog'
const { t } = useI18n() // 国际化
// 列表相关的变量
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: SmsLoglApi.getSmsLogPageApi,
exportListApi: SmsLoglApi.exportSmsLogApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
// ========== 详情相关 ==========
const detailData = ref() // 详情 Ref
const handleDetail = (row: SmsLoglApi.SmsLogVO) => {
// 设置数据
detailData.value = row
dialogVisible.value = true
}
</script>

View File

@@ -0,0 +1,82 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '日志编号',
action: true,
columns: [
{
title: '手机号',
field: 'mobile',
isSearch: true
},
{
title: '短信内容',
field: 'templateContent'
},
{
title: '模板编号',
field: 'templateId',
isSearch: true
},
{
title: '短信渠道',
field: 'channelId',
dictType: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE,
dictClass: 'number',
isSearch: true
},
{
title: '发送状态',
field: 'sendStatus',
dictType: DICT_TYPE.SYSTEM_SMS_SEND_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '发送时间',
field: 'sendTime',
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: '短信类型',
field: 'templateType',
dictType: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE,
dictClass: 'number',
isSearch: true
},
{
title: '接收状态',
field: 'receiveStatus',
dictType: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '接收时间',
field: 'receiveTime',
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate'
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,232 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:sms-channel:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<XTextButton
preIcon="ep:cpu"
:title="t('action.test')"
v-hasPermi="['system:sms-template:send-sms']"
@click="handleSendSms(row)"
/>
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:sms-template:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:sms-template:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:sms-template:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal id="smsTemplate" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
<XModal id="sendTest" v-model="sendVisible" title="测试">
<el-form :model="sendSmsForm" :rules="sendSmsRules" label-width="200px" label-position="top">
<el-form-item label="模板内容" prop="content">
<el-input
v-model="sendSmsForm.content"
type="textarea"
placeholder="请输入模板内容"
readonly
/>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="sendSmsForm.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item
v-for="param in sendSmsForm.params"
:key="param"
:label="'参数 {' + param + '}'"
:prop="'templateParams.' + param"
>
<el-input
v-model="sendSmsForm.templateParams[param]"
:placeholder="'请输入 ' + param + ' 参数'"
/>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<XButton
type="primary"
:title="t('action.test')"
:loading="actionLoading"
@click="sendSmsTest()"
/>
<XButton :title="t('dialog.close')" @click="sendVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="SmsTemplate">
import type { FormExpose } from '@/components/Form'
// 业务相关的 import
import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
import { rules, allSchemas } from './sms.template.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: SmsTemplateApi.getSmsTemplatePageApi,
deleteApi: SmsTemplateApi.deleteSmsTemplateApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await SmsTemplateApi.getSmsTemplateApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
// 设置数据
const res = await SmsTemplateApi.getSmsTemplateApi(rowId)
detailData.value = res
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as SmsTemplateApi.SmsTemplateVO
if (actionType.value === 'create') {
await SmsTemplateApi.createSmsTemplateApi(data)
message.success(t('common.createSuccess'))
} else {
await SmsTemplateApi.updateSmsTemplateApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
// ========== 测试相关 ==========
const sendSmsForm = ref({
content: '',
params: {},
mobile: '',
templateCode: '',
templateParams: {}
})
const sendSmsRules = ref({
mobile: [{ required: true, message: '手机不能为空', trigger: 'blur' }],
templateCode: [{ required: true, message: '模版编号不能为空', trigger: 'blur' }],
templateParams: {}
})
const sendVisible = ref(false)
const handleSendSms = (row: any) => {
sendSmsForm.value.content = row.content
sendSmsForm.value.params = row.params
sendSmsForm.value.templateCode = row.code
sendSmsForm.value.templateParams = row.params.reduce(function (obj, item) {
obj[item] = undefined
return obj
}, {})
sendSmsRules.value.templateParams = row.params.reduce(function (obj, item) {
obj[item] = { required: true, message: '参数 ' + item + ' 不能为空', trigger: 'change' }
return obj
}, {})
sendVisible.value = true
}
const sendSmsTest = async () => {
const data: SmsTemplateApi.SendSmsReqVO = {
mobile: sendSmsForm.value.mobile,
templateCode: sendSmsForm.value.templateCode,
templateParams: sendSmsForm.value.templateParams as unknown as Map<string, Object>
}
const res = await SmsTemplateApi.sendSmsApi(data)
if (res) {
message.success('提交发送成功!发送结果,见发送日志编号:' + res)
}
sendVisible.value = false
}
</script>

View File

@@ -0,0 +1,81 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
type: [required],
status: [required],
code: [required],
name: [required],
content: [required],
apiTemplateId: [required],
channelId: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '模板编号',
action: true,
actionWidth: '280',
columns: [
{
title: '模板编码',
field: 'code',
isSearch: true
},
{
title: '模板名称',
field: 'name',
isSearch: true
},
{
title: '模板内容',
field: 'content'
},
{
title: '短信 API 的模板编号',
field: 'apiTemplateId',
isSearch: true
},
{
title: '短信类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE,
dictClass: 'number',
isSearch: true,
table: {
width: 80
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true,
table: {
width: 80
}
},
{
title: t('form.remark'),
field: 'remark',
isTable: false
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,174 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:tenant:create']"
@click="handleCreate()"
/>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:tenant:export']"
@click="exportList('租户列表.xls')"
/>
</template>
<template #accountCount_default="{ row }">
<el-tag> {{ row.accountCount }} </el-tag>
</template>
<template #packageId_default="{ row }">
<el-tag v-if="row.packageId === 0" type="danger">系统租户</el-tag>
<el-tag v-else type="success"> {{ getPackageName(row.packageId) }} </el-tag>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:tenant:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:tenant:update']"
@click="handleDetail(row.id)"
/>
<!-- 操作删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:tenant:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
>
<template #packageId="{ row }">
<el-tag v-if="row.packageId === 0" type="danger">系统租户</el-tag>
<el-tag v-else type="success"> {{ getPackageName(row.packageId) }} </el-tag>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="Tenant">
import type { FormExpose } from '@/components/Form'
import * as TenantApi from '@/api/system/tenant'
import { rules, allSchemas, tenantPackageOption } from './tenant.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: TenantApi.getTenantPageApi,
deleteApi: TenantApi.deleteTenantApi,
exportListApi: TenantApi.exportTenantApi
})
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
const getPackageName = (packageId: number) => {
for (let item of tenantPackageOption) {
if (item.value === packageId) {
return item.label
}
}
return '未知套餐'
}
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
// 重置表单
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await TenantApi.getTenantApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
// 设置数据
const res = await TenantApi.getTenantApi(rowId)
detailData.value = res
setDialogTile('detail')
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as TenantApi.TenantVO
if (actionType.value === 'create') {
await TenantApi.createTenantApi(data)
message.success(t('common.createSuccess'))
} else {
await TenantApi.updateTenantApi(data)
message.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
</script>

View File

@@ -0,0 +1,135 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
import { getTenantPackageList, TenantPackageVO } from '@/api/system/tenantPackage'
import { ComponentOptions } from '@/types/components'
const { t } = useI18n() // 国际化
export const tenantPackageOption: ComponentOptions[] = []
const getTenantPackageOptions = async () => {
const res = await getTenantPackageList()
res.forEach((tenantPackage: TenantPackageVO) => {
tenantPackageOption.push({
key: tenantPackage.id,
value: tenantPackage.id,
label: tenantPackage.name
})
})
return tenantPackageOption
}
getTenantPackageOptions()
// 表单校验
export const rules = reactive({
name: [required],
packageId: [required],
contactName: [required],
contactMobile: [required],
accountCount: [required],
expireTime: [required],
domain: [required],
status: [required]
})
// CrudSchema.
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryTitle: '租户编号',
primaryType: 'seq',
action: true,
columns: [
{
title: '租户名称',
field: 'name',
isSearch: true
},
{
title: '租户套餐',
field: 'packageId',
table: {
slots: {
default: 'packageId_default'
}
},
form: {
component: 'Select',
componentProps: {
options: tenantPackageOption
}
}
},
{
title: '联系人',
field: 'contactName',
isSearch: true
},
{
title: '联系手机',
field: 'contactMobile',
isSearch: true
},
{
title: '用户名称',
field: 'username',
isTable: false,
isDetail: false
},
{
title: '用户密码',
field: 'password',
isTable: false,
isDetail: false,
form: {
component: 'InputPassword'
}
},
{
title: '账号额度',
field: 'accountCount',
table: {
slots: {
default: 'accountCount_default'
}
},
form: {
component: 'InputNumber'
}
},
{
title: '过期时间',
field: 'expireTime',
formatter: 'formatDate',
form: {
component: 'DatePicker',
componentProps: {
type: 'datetime',
valueFormat: 'x'
}
}
},
{
title: '绑定域名',
field: 'domain'
},
{
title: '租户状态',
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: t('table.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,168 @@
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<XTextButton preIcon="ep:edit" :title="t('action.edit')" @click="handleUpdate(row.id)" />
<XTextButton preIcon="ep:delete" :title="t('action.del')" @click="deleteData(row.id)" />
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
>
<template #menuIds>
<el-card class="w-120">
<template #header>
<div class="card-header">
全选/全不选:
<el-switch
v-model="treeNodeAll"
inline-prompt
active-text=""
inactive-text=""
@change="handleCheckedTreeNodeAll()"
/>
</div>
</template>
<el-tree
ref="treeRef"
node-key="id"
show-checkbox
:props="defaultProps"
:data="menuOptions"
empty-text="加载中请稍后"
/>
</el-card>
</template>
</Form>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="loading"
@click="submitForm()"
/>
<!-- 按钮关闭 -->
<XButton :loading="loading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="TenantPackage">
import { handleTree, defaultProps } from '@/utils/tree'
import type { FormExpose } from '@/components/Form'
import type { ElTree } from 'element-plus'
// 业务相关的 import
import { rules, allSchemas } from './tenantPackage.data'
import * as TenantPackageApi from '@/api/system/tenantPackage'
import { listSimpleMenusApi } from '@/api/system/menu'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const menuOptions = ref<any[]>([]) // 树形结构
const menuExpand = ref(false)
const menuNodeAll = ref(false)
const treeRef = ref<InstanceType<typeof ElTree>>()
const treeNodeAll = ref(false)
const formRef = ref<FormExpose>() // 表单 Ref
const loading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
// 全选/全不选
const handleCheckedTreeNodeAll = () => {
treeRef.value!.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
}
const getTree = async () => {
const res = await listSimpleMenusApi()
menuOptions.value = handleTree(res)
}
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: TenantPackageApi.getTenantPackageTypePageApi,
deleteApi: TenantPackageApi.deleteTenantPackageTypeApi
})
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
//重置菜单树
unref(treeRef)?.setCheckedKeys([])
menuExpand.value = false
menuNodeAll.value = false
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await TenantPackageApi.getTenantPackageApi(rowId)
unref(formRef)?.setValues(res)
// 设置选中
unref(treeRef)?.setCheckedKeys(res.menuIds)
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as TenantPackageApi.TenantPackageVO
data.menuIds = [
...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>),
...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>)
]
if (actionType.value === 'create') {
await TenantPackageApi.createTenantPackageTypeApi(data)
message.success(t('common.createSuccess'))
} else {
await TenantPackageApi.updateTenantPackageTypeApi(data)
message.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
} finally {
loading.value = false
// 刷新列表
await reload()
}
}
})
}
// ========== 初始化 ==========
onMounted(async () => {
await getTree()
})
// getList()
</script>

View File

@@ -0,0 +1,68 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
id: [required],
type: [required],
remark: [required],
status: [required],
menuIds: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '套餐编号',
action: true,
columns: [
{
title: '套餐名称',
field: 'name',
isSearch: true
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '菜单权限',
field: 'menuIds',
isTable: false
},
{
title: t('form.remark'),
field: 'remark',
isTable: false,
isSearch: true,
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: '创建时间',
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,547 @@
<template>
<div class="flex">
<el-card class="w-1/5 user" :gutter="12" shadow="always">
<template #header>
<div class="card-header">
<span>部门列表</span>
<XTextButton title="修改部门" @click="handleDeptEdit()" />
</div>
</template>
<el-input v-model="filterText" placeholder="搜索部门" />
<el-scrollbar height="650">
<el-tree
ref="treeRef"
node-key="id"
default-expand-all
:data="deptOptions"
:props="defaultProps"
:highlight-current="true"
:filter-node-method="filterNode"
:expand-on-click-node="false"
@node-click="handleDeptNodeClick"
/>
</el-scrollbar>
</el-card>
<el-card class="w-4/5 user" style="margin-left: 10px" :gutter="12" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ tableTitle }}</span>
</div>
</template>
<!-- 列表 -->
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:user:create']"
@click="handleCreate()"
/>
<!-- 操作导入用户 -->
<XButton
type="warning"
preIcon="ep:upload"
:title="t('action.import')"
v-hasPermi="['system:user:import']"
@click="importDialogVisible = true"
/>
<!-- 操作导出用户 -->
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:user:export']"
@click="exportList('用户数据.xls')"
/>
</template>
<template #status_default="{ row }">
<el-switch
v-model="row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(row)"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作编辑 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:user:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:user:update']"
@click="handleDetail(row.id)"
/>
<el-dropdown
class="p-0.5"
v-hasPermi="[
'system:user:update-password',
'system:permission:assign-user-role',
'system:user:delete'
]"
>
<XTextButton :title="t('action.more')" postIcon="ep:arrow-down" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<!-- 操作:重置密码 -->
<XTextButton
preIcon="ep:key"
title="重置密码"
v-hasPermi="['system:user:update-password']"
@click="handleResetPwd(row)"
/>
</el-dropdown-item>
<el-dropdown-item>
<!-- 操作:分配角色 -->
<XTextButton
preIcon="ep:key"
title="分配角色"
v-hasPermi="['system:permission:assign-user-role']"
@click="handleRole(row)"
/>
</el-dropdown-item>
<el-dropdown-item>
<!-- 操作:删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:user:delete']"
@click="deleteData(row.id)"
/>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</XTable>
</el-card>
</div>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:rules="rules"
:schema="allSchemas.formSchema"
ref="formRef"
>
<template #deptId="form">
<el-tree-select
node-key="id"
v-model="form['deptId']"
:props="defaultProps"
:data="deptOptions"
check-strictly
/>
</template>
<template #postIds="form">
<el-select v-model="form['postIds']" multiple :placeholder="t('common.selectText')">
<el-option
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="(item.id as unknown as number)"
/>
</el-select>
</template>
</Form>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
>
<template #deptId="{ row }">
<span>{{ row.dept?.name }}</span>
</template>
<template #postIds="{ row }">
<template v-if="row.postIds !== ''">
<el-tag v-for="(post, index) in row.postIds" :key="index" index="">
<template v-for="postObj in postOptions">
{{ post === postObj.id ? postObj.name : '' }}
</template>
</el-tag>
</template>
<template v-else> </template>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮:保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="loading"
@click="submitForm()"
/>
<!-- 按钮:关闭 -->
<XButton :loading="loading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
<!-- 分配用户角色 -->
<XModal v-model="roleDialogVisible" title="分配角色">
<el-form :model="userRole" label-width="140px" :inline="true">
<el-form-item label="用户名称">
<el-tag>{{ userRole.username }}</el-tag>
</el-form-item>
<el-form-item label="用户昵称">
<el-tag>{{ userRole.nickname }}</el-tag>
</el-form-item>
<el-form-item label="角色">
<el-transfer
v-model="userRole.roleIds"
:titles="['角色列表', '已选择']"
:props="{
key: 'id',
label: 'name'
}"
:data="roleOptions"
/>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮:保存 -->
<XButton type="primary" :title="t('action.save')" :loading="loading" @click="submitRole()" />
<!-- 按钮:关闭 -->
<XButton :title="t('dialog.close')" @click="roleDialogVisible = false" />
</template>
</XModal>
<!-- 导入 -->
<XModal v-model="importDialogVisible" :title="importDialogTitle">
<el-form class="drawer-multiColumn-form" label-width="150px">
<el-form-item label="模板下载 :">
<XButton type="primary" prefix="ep:download" title="点击下载" @click="handleImportTemp()" />
</el-form-item>
<el-form-item label="文件上传 :">
<el-upload
ref="uploadRef"
:action="updateUrl + '?updateSupport=' + updateSupport"
:headers="uploadHeaders"
:drag="true"
:limit="1"
:multiple="true"
:show-file-list="true"
:disabled="uploadDisabled"
:before-upload="beforeExcelUpload"
:on-exceed="handleExceed"
:on-success="handleFileSuccess"
:on-error="excelUploadError"
:auto-upload="false"
accept="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
>
<Icon icon="ep:upload-filled" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">请上传 .xls , .xlsx 标准格式文件</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="是否更新已经存在的用户数据:">
<el-checkbox v-model="updateSupport" />
</el-form-item>
</el-form>
<template #footer>
<!-- 按钮:保存 -->
<XButton
type="warning"
preIcon="ep:upload-filled"
:title="t('action.save')"
@click="submitFileForm()"
/>
<!-- 按钮:关闭 -->
<XButton :title="t('dialog.close')" @click="importDialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="User">
import type { ElTree, UploadRawFile, UploadInstance } from 'element-plus'
import { handleTree, defaultProps } from '@/utils/tree'
import download from '@/utils/download'
import { CommonStatusEnum } from '@/utils/constants'
import { getAccessToken, getTenantId } from '@/utils/auth'
import type { FormExpose } from '@/components/Form'
import { rules, allSchemas } from './user.data'
import * as UserApi from '@/api/system/user'
import { listSimpleDeptApi } from '@/api/system/dept'
import { listSimpleRolesApi } from '@/api/system/role'
import { listSimplePostsApi, PostVO } from '@/api/system/post'
import {
aassignUserRoleApi,
listUserRolesApi,
PermissionAssignUserRoleReqVO
} from '@/api/system/permission'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const queryParams = reactive({
deptId: null
})
// ========== 列表相关 ==========
const tableTitle = ref('用户列表')
// 列表相关的变量
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
params: queryParams,
getListApi: UserApi.getUserPageApi,
deleteApi: UserApi.deleteUserApi,
exportListApi: UserApi.exportUserApi
})
// ========== 创建部门树结构 ==========
const filterText = ref('')
const deptOptions = ref<Tree[]>([]) // 树形结构
const treeRef = ref<InstanceType<typeof ElTree>>()
const getTree = async () => {
const res = await listSimpleDeptApi()
deptOptions.value.push(...handleTree(res))
}
const filterNode = (value: string, data: Tree) => {
if (!value) return true
return data.name.includes(value)
}
const handleDeptNodeClick = async (row: { [key: string]: any }) => {
queryParams.deptId = row.id
await reload()
}
const { push } = useRouter()
const handleDeptEdit = () => {
push('/system/dept')
}
watch(filterText, (val) => {
treeRef.value!.filter(val)
})
// ========== CRUD 相关 ==========
const loading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
const postOptions = ref<PostVO[]>([]) //岗位列表
// 获取岗位列表
const getPostOptions = async () => {
const res = await listSimplePostsApi()
postOptions.value.push(...res)
}
// 设置标题
const setDialogTile = async (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = async () => {
setDialogTile('create')
// 重置表单
await nextTick()
if (allSchemas.formSchema[0].field !== 'username') {
unref(formRef)?.addSchema(
{
field: 'username',
label: '用户账号',
component: 'Input'
},
0
)
unref(formRef)?.addSchema(
{
field: 'password',
label: '用户密码',
component: 'InputPassword'
},
1
)
}
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
await nextTick()
unref(formRef)?.delSchema('username')
unref(formRef)?.delSchema('password')
// 设置数据
const res = await UserApi.getUserApi(rowId)
unref(formRef)?.setValues(res)
}
const detailData = ref()
// 详情操作
const handleDetail = async (rowId: number) => {
// 设置数据
const res = await UserApi.getUserApi(rowId)
detailData.value = res
await setDialogTile('detail')
}
// 提交按钮
const submitForm = async () => {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as UserApi.UserVO
if (actionType.value === 'create') {
await UserApi.createUserApi(data)
message.success(t('common.createSuccess'))
} else {
await UserApi.updateUserApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
// unref(formRef)?.setSchema(allSchemas.formSchema)
// 刷新列表
await reload()
loading.value = false
}
}
// 改变用户状态操作
const handleStatusChange = async (row: UserApi.UserVO) => {
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
message
.confirm('确认要"' + text + '""' + row.username + '"用户吗?', t('common.reminder'))
.then(async () => {
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.ENABLE : CommonStatusEnum.DISABLE
await UserApi.updateUserStatusApi(row.id, row.status)
message.success(text + '成功')
// 刷新列表
await reload()
})
.catch(() => {
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
})
}
// 重置密码
const handleResetPwd = (row: UserApi.UserVO) => {
message.prompt('请输入"' + row.username + '"的新密码', t('common.reminder')).then(({ value }) => {
UserApi.resetUserPwdApi(row.id, value).then(() => {
message.success('修改成功,新密码是:' + value)
})
})
}
// 分配角色
const roleDialogVisible = ref(false)
const roleOptions = ref()
const userRole = reactive({
id: 0,
username: '',
nickname: '',
roleIds: []
})
const handleRole = async (row: UserApi.UserVO) => {
userRole.id = row.id
userRole.username = row.username
userRole.nickname = row.nickname
// 获得角色拥有的权限集合
const roles = await listUserRolesApi(row.id)
userRole.roleIds = roles
// 获取角色列表
const roleOpt = await listSimpleRolesApi()
roleOptions.value = roleOpt
roleDialogVisible.value = true
}
// 提交
const submitRole = async () => {
const data = ref<PermissionAssignUserRoleReqVO>({
userId: userRole.id,
roleIds: userRole.roleIds
})
await aassignUserRoleApi(data.value)
message.success(t('common.updateSuccess'))
roleDialogVisible.value = false
}
// ========== 导入相关 ==========
// TODO @星语:这个要不要把导入用户,封装成一个小组件?可选哈
const importDialogVisible = ref(false)
const uploadDisabled = ref(false)
const importDialogTitle = ref('用户导入')
const updateSupport = ref(0)
let updateUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/user/import'
const uploadHeaders = ref()
// 下载导入模版
const handleImportTemp = async () => {
const res = await UserApi.importUserTemplateApi()
download.excel(res, '用户导入模版.xls')
}
// 文件上传之前判断
const beforeExcelUpload = (file: UploadRawFile) => {
const isExcel =
file.type === 'application/vnd.ms-excel' ||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
const isLt5M = file.size / 1024 / 1024 < 5
if (!isExcel) message.error('上传文件只能是 xls / xlsx 格式!')
if (!isLt5M) message.error('上传文件大小不能超过 5MB!')
return isExcel && isLt5M
}
// 文件上传
const uploadRef = ref<UploadInstance>()
const submitFileForm = () => {
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
uploadDisabled.value = true
uploadRef.value!.submit()
}
// 文件上传成功
const handleFileSuccess = async (response: any): Promise<void> => {
if (response.code !== 0) {
message.error(response.msg)
return
}
importDialogVisible.value = false
uploadDisabled.value = false
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)
await reload()
}
// 文件数超出提示
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
// 上传错误提示
const excelUploadError = (): void => {
message.error('导入数据失败,请您重新上传!')
}
// ========== 初始化 ==========
onMounted(async () => {
await getPostOptions()
await getTree()
})
</script>
<style scoped>
.user {
height: 780px;
max-height: 800px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -0,0 +1,107 @@
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const rules = reactive({
username: [required],
nickname: [required],
email: [required],
status: [required],
mobile: [
{
len: 11,
trigger: 'blur',
message: '请输入正确的手机号码'
}
]
})
// crudSchemas
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
primaryTitle: '用户编号',
action: true,
actionWidth: '200px',
columns: [
{
title: '用户账号',
field: 'username',
isSearch: true
},
{
title: '用户密码',
field: 'password',
isDetail: false,
isTable: false,
form: {
component: 'InputPassword'
}
},
{
title: '用户昵称',
field: 'nickname'
},
{
title: '用户邮箱',
field: 'email'
},
{
title: '手机号码',
field: 'mobile',
isSearch: true
},
{
title: '部门',
field: 'deptId',
isTable: false
},
{
title: '岗位',
field: 'postIds',
isTable: false
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true,
table: {
slots: {
default: 'status_default'
}
}
},
{
title: '最后登录时间',
field: 'loginDate',
formatter: 'formatDate',
isForm: false
},
{
title: '最后登录IP',
field: 'loginIp',
isTable: false,
isForm: false
},
{
title: t('form.remark'),
field: 'remark',
isTable: false
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isTable: false,
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)