Commit a1609e30 by fengjingtao

Merge branch 'dev' of https://gitee.com/fessor/yudao-ui-admin-vue3 into dev

parents 29fd92e6 b1e74a1d
...@@ -6,4 +6,6 @@ dist-ssr ...@@ -6,4 +6,6 @@ dist-ssr
/dist* /dist*
*-lock.* *-lock.*
pnpm-debug pnpm-debug
.idea .idea
.history
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
"@form-create/designer": "^3.1.0", "@form-create/designer": "^3.1.0",
"@form-create/element-ui": "^3.1.17", "@form-create/element-ui": "^3.1.17",
"@iconify/iconify": "^3.1.0", "@iconify/iconify": "^3.1.0",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^9.13.0", "@vueuse/core": "^9.13.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10", "@wangeditor/editor-for-vue": "^5.1.10",
...@@ -58,6 +59,7 @@ ...@@ -58,6 +59,7 @@
"qs": "^6.11.1", "qs": "^6.11.1",
"steady-xml": "^0.1.0", "steady-xml": "^0.1.0",
"url": "^0.11.0", "url": "^0.11.0",
"video.js": "^8.0.4",
"vue": "3.2.47", "vue": "3.2.47",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
......
...@@ -4,6 +4,7 @@ export type Task = { ...@@ -4,6 +4,7 @@ export type Task = {
id: string id: string
name: string name: string
} }
export type ProcessInstanceVO = { export type ProcessInstanceVO = {
id: number id: number
name: string name: string
......
import request from '@/config/axios' import request from '@/config/axios'
import type { CodegenUpdateReqVO, CodegenCreateListReqVO } from './types'
export type CodegenTableVO = {
id: number
tableId: number
isParentMenuIdValid: boolean
dataSourceConfigId: number
scene: number
tableName: string
tableComment: string
remark: string
moduleName: string
businessName: string
className: string
classComment: string
author: string
createTime: Date
updateTime: Date
templateType: number
parentMenuId: number
}
export type CodegenColumnVO = {
id: number
tableId: number
columnName: string
dataType: string
columnComment: string
nullable: number
primaryKey: number
autoIncrement: string
ordinalPosition: number
javaType: string
javaField: string
dictType: string
example: string
createOperation: number
updateOperation: number
listOperation: number
listOperationCondition: string
listOperationResult: number
htmlType: string
}
export type DatabaseTableVO = {
name: string
comment: string
}
export type CodegenDetailVO = {
table: CodegenTableVO
columns: CodegenColumnVO[]
}
export type CodegenPreviewVO = {
filePath: string
code: string
}
export type CodegenUpdateReqVO = {
table: CodegenTableVO | any
columns: CodegenColumnVO[]
}
export type CodegenCreateListReqVO = {
dataSourceConfigId: number
tableNames: string[]
}
// 查询列表代码生成表定义 // 查询列表代码生成表定义
export const getCodegenTablePageApi = (params) => { export const getCodegenTablePage = (params: PageParam) => {
return request.get({ url: '/infra/codegen/table/page', params }) return request.get({ url: '/infra/codegen/table/page', params })
} }
// 查询详情代码生成表定义 // 查询详情代码生成表定义
export const getCodegenTableApi = (id: number) => { export const getCodegenTable = (id: number) => {
return request.get({ url: '/infra/codegen/detail?tableId=' + id }) return request.get({ url: '/infra/codegen/detail?tableId=' + id })
} }
// 新增代码生成表定义 // 新增代码生成表定义
export const createCodegenTableApi = (data: CodegenCreateListReqVO) => { export const createCodegenTable = (data: CodegenCreateListReqVO) => {
return request.post({ url: '/infra/codegen/create', data }) return request.post({ url: '/infra/codegen/create', data })
} }
// 修改代码生成表定义 // 修改代码生成表定义
export const updateCodegenTableApi = (data: CodegenUpdateReqVO) => { export const updateCodegenTable = (data: CodegenUpdateReqVO) => {
return request.put({ url: '/infra/codegen/update', data }) return request.put({ url: '/infra/codegen/update', data })
} }
// 基于数据库的表结构,同步数据库的表和字段定义 // 基于数据库的表结构,同步数据库的表和字段定义
export const syncCodegenFromDBApi = (id: number) => { export const syncCodegenFromDB = (id: number) => {
return request.put({ url: '/infra/codegen/sync-from-db?tableId=' + id }) return request.put({ url: '/infra/codegen/sync-from-db?tableId=' + id })
} }
// 基于 SQL 建表语句,同步数据库的表和字段定义 // 基于 SQL 建表语句,同步数据库的表和字段定义
export const syncCodegenFromSQLApi = (id: number, sql: string) => { export const syncCodegenFromSQL = (id: number, sql: string) => {
return request.put({ url: '/infra/codegen/sync-from-sql?tableId=' + id + '&sql=' + sql }) return request.put({ url: '/infra/codegen/sync-from-sql?tableId=' + id + '&sql=' + sql })
} }
// 预览生成代码 // 预览生成代码
export const previewCodegenApi = (id: number) => { export const previewCodegen = (id: number) => {
return request.get({ url: '/infra/codegen/preview?tableId=' + id }) return request.get({ url: '/infra/codegen/preview?tableId=' + id })
} }
// 下载生成代码 // 下载生成代码
export const downloadCodegenApi = (id: number) => { export const downloadCodegen = (id: number) => {
return request.download({ url: '/infra/codegen/download?tableId=' + id }) return request.download({ url: '/infra/codegen/download?tableId=' + id })
} }
// 获得表定义 // 获得表定义
export const getSchemaTableListApi = (params) => { export const getSchemaTableList = (params) => {
return request.get({ url: '/infra/codegen/db/table/list', params }) return request.get({ url: '/infra/codegen/db/table/list', params })
} }
// 基于数据库的表结构,创建代码生成器的表定义 // 基于数据库的表结构,创建代码生成器的表定义
export const createCodegenListApi = (data) => { export const createCodegenList = (data) => {
return request.post({ url: '/infra/codegen/create-list', data }) return request.post({ url: '/infra/codegen/create-list', data })
} }
// 删除代码生成表定义 // 删除代码生成表定义
export const deleteCodegenTableApi = (id: number) => { export const deleteCodegenTable = (id: number) => {
return request.delete({ url: '/infra/codegen/delete?tableId=' + id }) return request.delete({ url: '/infra/codegen/delete?tableId=' + id })
} }
export type CodegenTableVO = {
id: number
tableId: number
isParentMenuIdValid: boolean
dataSourceConfigId: number
scene: number
tableName: string
tableComment: string
remark: string
moduleName: string
businessName: string
className: string
classComment: string
author: string
createTime: Date
updateTime: Date
templateType: number
parentMenuId: number
}
export type CodegenColumnVO = {
id: number
tableId: number
columnName: string
dataType: string
columnComment: string
nullable: number
primaryKey: number
autoIncrement: string
ordinalPosition: number
javaType: string
javaField: string
dictType: string
example: string
createOperation: number
updateOperation: number
listOperation: number
listOperationCondition: string
listOperationResult: number
htmlType: string
}
export type DatabaseTableVO = {
name: string
comment: string
}
export type CodegenDetailVO = {
table: CodegenTableVO
columns: CodegenColumnVO[]
}
export type CodegenPreviewVO = {
filePath: string
code: string
}
export type CodegenUpdateReqVO = {
table: CodegenTableVO
columns: CodegenColumnVO[]
}
export type CodegenCreateListReqVO = {
dataSourceConfigId: number
tableNames: string[]
}
...@@ -17,7 +17,7 @@ export interface FileClientConfig { ...@@ -17,7 +17,7 @@ export interface FileClientConfig {
export interface FileConfigVO { export interface FileConfigVO {
id: number id: number
name: string name: string
storage: number storage: any
master: boolean master: boolean
visible: boolean visible: boolean
config: FileClientConfig config: FileClientConfig
......
...@@ -15,31 +15,13 @@ export interface NotifyMessageVO { ...@@ -15,31 +15,13 @@ export interface NotifyMessageVO {
readTime: Date readTime: Date
} }
export interface NotifyMessagePageReqVO extends PageParam {
userId?: number
userType?: number
templateCode?: string
templateType?: number
createTime?: Date[]
}
export interface NotifyMessageMyPageReqVO extends PageParam {
readStatus?: boolean
createTime?: Date[]
}
// 查询站内信消息列表 // 查询站内信消息列表
export const getNotifyMessagePageApi = async (params: NotifyMessagePageReqVO) => { export const getNotifyMessagePage = async (params: PageParam) => {
return await request.get({ url: '/system/notify-message/page', params }) return await request.get({ url: '/system/notify-message/page', params })
} }
// 查询站内信消息详情
export const getNotifyMessageApi = async (id: number) => {
return await request.get({ url: '/system/notify-message/get?id=' + id })
}
// 获得我的站内信分页 // 获得我的站内信分页
export const getMyNotifyMessagePage = async (params: NotifyMessageMyPageReqVO) => { export const getMyNotifyMessagePage = async (params: PageParam) => {
return await request.get({ url: '/system/notify-message/my-page', params }) return await request.get({ url: '/system/notify-message/my-page', params })
} }
......
...@@ -10,6 +10,8 @@ import { ...@@ -10,6 +10,8 @@ import {
ElTransfer, ElTransfer,
ElAlert, ElAlert,
ElTabs, ElTabs,
ElTable,
ElTableColumn,
ElTabPane ElTabPane
} from 'element-plus' } from 'element-plus'
...@@ -27,6 +29,8 @@ const components = [ ...@@ -27,6 +29,8 @@ const components = [
ElTransfer, ElTransfer,
ElAlert, ElAlert,
ElTabs, ElTabs,
ElTable,
ElTableColumn,
ElTabPane ElTabPane
] ]
......
...@@ -272,7 +272,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -272,7 +272,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
}, },
{ {
path: '/process-instance/create', path: '/process-instance/create',
component: () => import('@/views/bpm/processInstance/create.vue'), component: () => import('@/views/bpm/processInstance/create/index.vue'),
name: 'BpmProcessInstanceCreate', name: 'BpmProcessInstanceCreate',
meta: { meta: {
noCache: true, noCache: true,
...@@ -284,7 +284,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -284,7 +284,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
}, },
{ {
path: '/process-instance/detail', path: '/process-instance/detail',
component: () => import('@/views/bpm/processInstance/detail.vue'), component: () => import('@/views/bpm/processInstance/detail/index.vue'),
name: 'BpmProcessInstanceDetail', name: 'BpmProcessInstanceDetail',
meta: { meta: {
noCache: true, noCache: true,
......
...@@ -68,6 +68,8 @@ declare module '@vue/runtime-core' { ...@@ -68,6 +68,8 @@ declare module '@vue/runtime-core' {
ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs'] ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTransfer: typeof import('element-plus/es')['ElTransfer'] ElTransfer: typeof import('element-plus/es')['ElTransfer']
ElTree: typeof import('element-plus/es')['ElTree'] ElTree: typeof import('element-plus/es')['ElTree']
......
...@@ -196,3 +196,54 @@ export const dateFormatter = (row, column, cellValue) => { ...@@ -196,3 +196,54 @@ export const dateFormatter = (row, column, cellValue) => {
} }
return formatDate(cellValue) return formatDate(cellValue)
} }
/**
* 设置起始日期,时间为00:00:00
* @param param 传入日期
* @returns 带时间00:00:00的日期
*/
export function beginOfDay(param: Date) {
return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 0, 0, 0, 0)
}
/**
* 设置结束日期,时间为23:59:59
* @param param 传入日期
* @returns 带时间23:59:59的日期
*/
export function endOfDay(param: Date) {
return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 23, 59, 59, 999)
}
/**
* 计算两个日期间隔天数
* @param param1 日期1
* @param param2 日期2
*/
export function betweenDay(param1: Date, param2: Date) {
param1 = convertDate(param1)
param2 = convertDate(param2)
// 计算差值
return Math.floor((param2.getTime() - param1.getTime()) / (24 * 3600 * 1000))
}
/**
* 日期计算
* @param param1 日期
* @param param2 添加的时间
*/
export function addTime(param1: Date, param2: number) {
param1 = convertDate(param1)
return new Date(param1.getTime() + param2)
}
/**
* 日期转换
* @param param 日期
*/
export function convertDate(param: Date | string) {
if (typeof param === 'string') {
return new Date(param)
}
return param
}
...@@ -110,7 +110,7 @@ const queryParams = reactive({ ...@@ -110,7 +110,7 @@ const queryParams = reactive({
key: query.key key: query.key
}) })
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -103,7 +103,7 @@ const queryParams = reactive({ ...@@ -103,7 +103,7 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -132,7 +132,7 @@ const queryParams = reactive({ ...@@ -132,7 +132,7 @@ const queryParams = reactive({
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const userList = ref([]) // 用户列表 const userList = ref([]) // 用户列表
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -248,7 +248,7 @@ const queryParams = reactive({ ...@@ -248,7 +248,7 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
<template> <template>
<ContentWrap> <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
<!-- 第一步,通过流程定义的列表,选择对应的流程 --> <ContentWrap v-if="!selectProcessInstance">
<div v-if="!selectProcessInstance"> <el-table v-loading="loading" :data="list">
<XTable @register="registerTable"> <el-table-column label="流程名称" align="center" prop="name" />
<!-- 流程分类 --> <el-table-column label="流程分类" align="center" prop="category">
<template #category_default="{ row }"> <template #default="scope">
<DictTag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="Number(row?.category)" /> <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
</template> </template>
<template #version_default="{ row }"> </el-table-column>
<el-tag v-if="row">v{{ row.version }}</el-tag> <el-table-column label="流程版本" align="center" prop="version">
<template #default="scope">
<el-tag>v{{ scope.row.version }}</el-tag>
</template> </template>
<template #actionbtns_default="{ row }"> </el-table-column>
<XTextButton preIcon="ep:plus" title="选择" @click="handleSelect(row)" /> <el-table-column label="流程描述" align="center" prop="description" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button link type="primary" @click="handleSelect(scope.row)">
<Icon icon="ep:plus" /> 选择
</el-button>
</template> </template>
</XTable> </el-table-column>
</div> </el-table>
<!-- 第二步,填写表单,进行流程的提交 --> </ContentWrap>
<div v-else>
<el-card class="box-card"> <!-- 第二步,填写表单,进行流程的提交 -->
<div class="clearfix"> <ContentWrap v-else>
<span class="el-icon-document">申请信息【{{ selectProcessInstance.name }}】</span> <el-card class="box-card">
<XButton <div class="clearfix">
style="float: right" <span class="el-icon-document">申请信息【{{ selectProcessInstance.name }}】</span>
type="primary" <el-button style="float: right" type="primary" @click="selectProcessInstance = undefined">
preIcon="ep:delete" <Icon icon="ep:delete" /> 选择其它流程
title="选择其它流程" </el-button>
@click="selectProcessInstance = undefined" </div>
/> <el-col :span="16" :offset="6" style="margin-top: 20px">
</div> <form-create
<el-col :span="16" :offset="6" style="margin-top: 20px"> :rule="detailForm.rule"
<form-create v-model:api="fApi"
:rule="detailForm.rule" :option="detailForm.option"
v-model:api="fApi" @submit="submitForm"
:option="detailForm.option"
@submit="submitForm"
/>
</el-col>
</el-card>
<el-card class="box-card">
<div class="clearfix">
<span class="el-icon-picture-outline">流程图</span>
</div>
<!-- TODO 芋艿:待完成??? -->
<my-process-viewer
key="designer"
v-model="bpmnXML"
:value="bpmnXML"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/> />
</el-card> </el-col>
</div> </el-card>
<!-- 流程图预览 -->
<ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML" />
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// 业务相关的 import import { DICT_TYPE } from '@/utils/dict'
import { allSchemas } from './process.create'
import * as DefinitionApi from '@/api/bpm/definition' import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance' import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { setConfAndFields2 } from '@/utils/formCreate' import { setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config' import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { DICT_TYPE } from '@/utils/dict' import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
const router = useRouter() // 路由 const router = useRouter() // 路由
const message = useMessage() // 消息 const message = useMessage() // 消息
// ========== 列表相关 ========== // ========== 列表相关 ==========
const loading = ref(true) // 列表的加载中
const [registerTable] = useXTable({ const list = ref([]) // 列表的数据
allSchemas: allSchemas, const queryParams = reactive({
params: { suspensionState: 1
suspensionState: 1
},
getListApi: DefinitionApi.getProcessDefinitionList,
isList: true
}) })
// ========== 表单相关 ========== /** 查询列表 */
const getList = async () => {
loading.value = true
try {
list.value = await DefinitionApi.getProcessDefinitionList(queryParams)
} finally {
loading.value = false
}
}
// ========== 表单相关 ==========
const bpmnXML = ref(null) // BPMN 数据
const fApi = ref<ApiAttrs>() const fApi = ref<ApiAttrs>()
// 流程表单详情
const detailForm = ref({ const detailForm = ref({
// 流程表单详情
rule: [], rule: [],
option: {} option: {}
}) })
// 流程表单
const selectProcessInstance = ref() // 选择的流程实例 const selectProcessInstance = ref() // 选择的流程实例
/** 处理选择流程的按钮操作 **/ /** 处理选择流程的按钮操作 **/
const handleSelect = async (row) => { const handleSelect = async (row) => {
// 设置选择的流程 // 设置选择的流程
...@@ -97,11 +92,8 @@ const handleSelect = async (row) => { ...@@ -97,11 +92,8 @@ const handleSelect = async (row) => {
if (row.formType == 10) { if (row.formType == 10) {
// 设置表单 // 设置表单
setConfAndFields2(detailForm, row.formConf, row.formFields) setConfAndFields2(detailForm, row.formConf, row.formFields)
// 加载流程图 // 加载流程图
DefinitionApi.getProcessDefinitionBpmnXML(row.id).then((response) => { bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
bpmnXML.value = response
})
// 情况二:业务表单 // 情况二:业务表单
} else if (row.formCustomCreatePath) { } else if (row.formCustomCreatePath) {
await router.push({ await router.push({
...@@ -116,7 +108,6 @@ const submitForm = async (formData) => { ...@@ -116,7 +108,6 @@ const submitForm = async (formData) => {
if (!fApi.value || !selectProcessInstance.value) { if (!fApi.value || !selectProcessInstance.value) {
return return
} }
// 提交请求 // 提交请求
fApi.value.btn.loading(true) fApi.value.btn.loading(true)
try { try {
...@@ -126,29 +117,14 @@ const submitForm = async (formData) => { ...@@ -126,29 +117,14 @@ const submitForm = async (formData) => {
}) })
// 提示 // 提示
message.success('发起流程成功') message.success('发起流程成功')
// this.$tab.closeOpenPage();
router.go(-1) router.go(-1)
} finally { } finally {
fApi.value.btn.loading(false) fApi.value.btn.loading(false)
} }
} }
// ========== 流程图相关 ========== /** 初始化 */
onMounted(() => {
// // BPMN 数据 getList()
const bpmnXML = ref(null)
const bpmnControlForm = ref({
prefix: 'flowable'
}) })
</script> </script>
<style lang="scss">
.my-process-designer {
height: calc(100vh - 200px);
}
.box-card {
width: 100%;
margin-bottom: 20px;
}
</style>
<template>
<el-card class="box-card" v-loading="loading">
<template #header>
<span class="el-icon-picture-outline">流程图</span>
</template>
<my-process-viewer
key="designer"
:value="bpmnXml"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
:activityData="activityList"
:processInstanceData="processInstance"
:taskData="tasks"
/>
</el-card>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import * as ActivityApi from '@/api/bpm/activity'
// import * as DefinitionApi from '@/api/bpm/definition'
const props = defineProps({
loading: propTypes.bool, // 是否加载中
id: propTypes.string, // 流程实例的编号
processInstance: propTypes.any, // 流程实例的信息
tasks: propTypes.array, // 流程任务的数组
bpmnXml: propTypes.string // BPMN XML
})
const bpmnControlForm = ref({
prefix: 'flowable'
})
const activityList = ref([]) // 任务列表
// const bpmnXML = computed(() => { // TODO 芋艿:不晓得为啊哈不能这么搞
// if (!props.processInstance || !props.processInstance.processDefinition) {
// return
// }
// return DefinitionApi.getProcessDefinitionBpmnXML(props.processInstance.processDefinition.id)
// })
/** 初始化 */
onMounted(async () => {
if (props.id) {
activityList.value = await ActivityApi.getActivityList({
processInstanceId: props.id
})
}
})
</script>
<style>
.box-card {
width: 100%;
margin-bottom: 20px;
}
</style>
<template>
<el-card class="box-card" v-loading="loading">
<template #header>
<span class="el-icon-picture-outline">审批记录</span>
</template>
<el-col :span="16" :offset="4">
<div class="block">
<el-timeline>
<el-timeline-item
v-for="(item, index) in tasks"
:key="index"
:icon="getTimelineItemIcon(item)"
:type="getTimelineItemType(item)"
>
<p style="font-weight: 700">任务:{{ item.name }}</p>
<el-card :body-style="{ padding: '10px' }">
<label v-if="item.assigneeUser" style="font-weight: normal; margin-right: 30px">
审批人:{{ item.assigneeUser.nickname }}
<el-tag type="info" size="small">{{ item.assigneeUser.deptName }}</el-tag>
</label>
<label style="font-weight: normal" v-if="item.createTime">创建时间:</label>
<label style="color: #8a909c; font-weight: normal">
{{ parseTime(item?.createTime) }}
</label>
<label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
审批时间:
</label>
<label v-if="item.endTime" style="color: #8a909c; font-weight: normal">
{{ parseTime(item?.endTime) }}
</label>
<label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
耗时:
</label>
<label v-if="item.durationInMillis" style="color: #8a909c; font-weight: normal">
{{ formatPast2(item?.durationInMillis) }}
</label>
<p v-if="item.reason">
<el-tag :type="getTimelineItemType(item)">{{ item.reason }}</el-tag>
</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-col>
</el-card>
</template>
<script setup lang="ts">
import { parseTime, formatPast2 } from '@/utils/formatTime'
import { propTypes } from '@/utils/propTypes'
defineProps({
loading: propTypes.bool, // 是否加载中
tasks: propTypes.array // 流程任务的数组
})
/** 获得任务对应的 icon */
const getTimelineItemIcon = (item) => {
if (item.result === 1) {
return 'el-icon-time'
}
if (item.result === 2) {
return 'el-icon-check'
}
if (item.result === 3) {
return 'el-icon-close'
}
if (item.result === 4) {
return 'el-icon-remove-outline'
}
return ''
}
/** 获得任务对应的颜色 */
const getTimelineItemType = (item) => {
if (item.result === 1) {
return 'primary'
}
if (item.result === 2) {
return 'success'
}
if (item.result === 3) {
return 'danger'
}
if (item.result === 4) {
return 'info'
}
return ''
}
</script>
<template>
<Dialog title="转派审批人" v-model="modelVisible" width="500">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="110px"
v-loading="formLoading"
>
<el-form-item label="新审批人" prop="assigneeUserId">
<el-select v-model="formData.assigneeUserId" clearable style="width: 100%">
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
const modelVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
assigneeUserId: undefined
})
const formRules = ref({
assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }]
})
const formRef = ref() // 表单 Ref
const userList = ref<any[]>([]) // 用户列表
/** 打开弹窗 */
const open = async (id: string) => {
modelVisible.value = true
resetForm()
formData.value.id = id
// 获得用户列表
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.updateTaskAssignee(formData.value)
modelVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
assigneeUserId: undefined
}
formRef.value?.resetFields()
}
</script>
<template>
<ContentWrap>
<!-- 审批信息 -->
<el-card
class="box-card"
v-loading="processInstanceLoading"
v-for="(item, index) in runningTasks"
:key="index"
>
<template #header>
<span class="el-icon-picture-outline">审批任务【{{ item.name }}</span>
</template>
<el-col :span="16" :offset="6">
<el-form
:ref="'form' + index"
:model="auditForms[index]"
:rules="auditRule"
label-width="100px"
>
<el-form-item label="流程名" v-if="processInstance && processInstance.name">
{{ processInstance.name }}
</el-form-item>
<el-form-item label="流程发起人" v-if="processInstance && processInstance.startUser">
{{ processInstance.startUser.nickname }}
<el-tag type="info" size="small">{{ processInstance.startUser.deptName }}</el-tag>
</el-form-item>
<el-form-item label="审批建议" prop="reason">
<el-input
type="textarea"
v-model="auditForms[index].reason"
placeholder="请输入审批建议"
/>
</el-form-item>
</el-form>
<div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
<el-button type="success" @click="handleAudit(item, true)">
<Icon icon="ep:select" /> 通过
</el-button>
<el-button type="danger" @click="handleAudit(item, false)">
<Icon icon="ep:close" /> 不通过
</el-button>
<el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
<Icon icon="ep:edit" /> 转办
</el-button>
<el-button type="primary" @click="handleDelegate(item)">
<Icon icon="ep:position" /> 委派
</el-button>
<el-button type="warning" @click="handleBack(item)">
<Icon icon="ep:back" /> 回退
</el-button>
</div>
</el-col>
</el-card>
<!-- 申请信息 -->
<el-card class="box-card" v-loading="processInstanceLoading">
<template #header>
<span class="el-icon-document">申请信息【{{ processInstance.name }}</span>
</template>
<!-- 情况一:流程表单 -->
<el-col v-if="processInstance?.processDefinition?.formType === 10" :span="16" :offset="6">
<form-create
ref="fApi"
:rule="detailForm.rule"
:option="detailForm.option"
v-model="detailForm.value"
/>
</el-col>
<!-- 情况二:流程表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<router-link
:to="
processInstance.processDefinition.formCustomViewPath +
'?id=' +
processInstance.businessKey
"
>
<el-button type="primary"><Icon icon="ep:view" /> 点击查看</el-button>
</router-link>
</div>
</el-card>
<!-- 审批记录 -->
<ProcessInstanceTaskList :loading="tasksLoad" :tasks="tasks" />
<!-- 高亮流程图 -->
<ProcessInstanceBpmnViewer
:id="id"
:process-instance="processInstance"
:loading="processInstanceLoading"
:tasks="tasks"
:bpmn-xml="bpmnXML"
/>
<!-- 弹窗:转派审批人 -->
<TaskUpdateAssigneeForm ref="taskUpdateAssigneeFormRef" @success="getDetail" />
</ContentWrap>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/modules/user'
import { setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as TaskApi from '@/api/bpm/task'
import TaskUpdateAssigneeForm from './TaskUpdateAssigneeForm.vue'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
const { query } = useRoute() // 查询参数
const message = useMessage() // 消息弹窗
const { proxy } = getCurrentInstance() as any
const userId = useUserStore().getUser.id // 当前登录的编号
const id = query.id as unknown as number // 流程实例的编号
const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例
const bpmnXML = ref('') // BPMN XML
const tasksLoad = ref(true) // 任务的加载中
const tasks = ref<any[]>([]) // 任务列表
// ========== 审批信息 ==========
const runningTasks = ref<any[]>([]) // 运行中的任务
const auditForms = ref<any[]>([]) // 审批任务的表单
const auditRule = reactive({
reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
})
// ========== 申请信息 ==========
const fApi = ref<ApiAttrs>() //
const detailForm = ref({
// 流程表单详情
rule: [],
option: {},
value: {}
})
/** 处理审批通过和不通过的操作 */
const handleAudit = async (task, pass) => {
// 1.1 获得对应表单
const index = runningTasks.value.indexOf(task)
const auditFormRef = proxy.$refs['form' + index][0]
// 1.2 校验表单
const elForm = unref(auditFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 2.1 提交审批
const data = {
id: task.id,
reason: auditForms.value[index].reason
}
if (pass) {
await TaskApi.approveTask(data)
message.success('审批通过成功')
} else {
await TaskApi.rejectTask(data)
message.success('审批不通过成功')
}
// 2.2 加载最新数据
getDetail()
}
/** 转派审批人 */
const taskUpdateAssigneeFormRef = ref()
const openTaskUpdateAssigneeForm = (id: string) => {
taskUpdateAssigneeFormRef.value.open(id)
}
/** 处理审批退回的操作 */
const handleDelegate = async (task) => {
message.error('暂不支持【委派】功能,可以使用【转派】替代!')
console.log(task)
}
/** 处理审批退回的操作 */
const handleBack = async (task) => {
message.error('暂不支持【退回】功能!')
console.log(task)
}
/** 获得详情 */
const getDetail = () => {
// 1. 获得流程实例相关
getProcessInstance()
// 2. 获得流程任务列表(审批记录)
getTaskList()
}
/** 加载流程实例 */
const getProcessInstance = async () => {
try {
processInstanceLoading.value = true
const data = await ProcessInstanceApi.getProcessInstanceApi(id)
if (!data) {
message.error('查询不到流程信息!')
return
}
processInstance.value = data
// 设置表单信息
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
setConfAndFields2(
detailForm,
processDefinition.formConf,
processDefinition.formFields,
data.formVariables
)
nextTick().then(() => {
fApi.value?.fapi?.btn.show(false)
fApi.value?.fapi?.resetBtn.show(false)
fApi.value?.fapi?.disabled(true)
})
}
// 加载流程图
bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id)
} finally {
processInstanceLoading.value = false
}
}
/** 加载任务列表 */
const getTaskList = async () => {
try {
// 获得未取消的任务
tasksLoad.value = true
const data = await TaskApi.getTaskListByProcessInstanceId(id)
tasks.value = []
// 1.1 移除已取消的审批
data.forEach((task) => {
if (task.result !== 4) {
tasks.value.push(task)
}
})
// 1.2 排序,将未完成的排在前面,已完成的排在后面;
tasks.value.sort((a, b) => {
// 有已完成的情况,按照完成时间倒序
if (a.endTime && b.endTime) {
return b.endTime - a.endTime
} else if (a.endTime) {
return 1
} else if (b.endTime) {
return -1
// 都是未完成,按照创建时间倒序
} else {
return b.createTime - a.createTime
}
})
// 获得需要自己审批的任务
runningTasks.value = []
auditForms.value = []
tasks.value.forEach((task) => {
// 2.1 只有待处理才需要
if (task.result !== 1) {
return
}
// 2.2 自己不是处理人
if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return
}
// 2.3 添加到处理任务
runningTasks.value.push({ ...task })
auditForms.value.push({
reason: ''
})
})
} finally {
tasksLoad.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// crudSchemas
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: null,
action: true,
columns: [
{
title: '流程名称',
field: 'name'
},
{
title: '流程分类',
field: 'category',
dictType: DICT_TYPE.BPM_MODEL_CATEGORY,
dictClass: 'number',
table: {
slots: {
default: 'category_default'
}
}
},
{
title: '流程版本',
field: 'version',
table: {
slots: {
default: 'version_default'
}
}
},
{
title: '流程描述',
field: 'description'
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
...@@ -56,7 +56,7 @@ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 ...@@ -56,7 +56,7 @@ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT) const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -182,7 +182,7 @@ const queryParams = reactive({ ...@@ -182,7 +182,7 @@ const queryParams = reactive({
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
<template> <template>
<ContentWrap> <content-wrap v-loading="formLoading">
<ContentDetailWrap :title="title" @back="push('/infra/codegen')"> <el-tabs v-model="activeName">
<el-tabs v-model="activeName"> <el-tab-pane label="基本信息" name="basicInfo">
<el-tab-pane label="基本信息" name="basicInfo"> <basic-info-form ref="basicInfoRef" :table="formData.table" />
<BasicInfoForm ref="basicInfoRef" :basicInfo="tableCurrentRow" /> </el-tab-pane>
</el-tab-pane> <el-tab-pane label="字段信息" name="colum">
<el-tab-pane label="字段信息" name="cloum"> <colum-info-form ref="columInfoRef" :columns="formData.columns" />
<CloumInfoForm ref="cloumInfoRef" :info="cloumCurrentRow" /> </el-tab-pane>
</el-tab-pane> <el-tab-pane label="生成信息" name="generateInfo">
</el-tabs> <generate-info-form ref="generateInfoRef" :table="formData.table" />
<template #right> </el-tab-pane>
<XButton </el-tabs>
type="primary" <el-form>
:title="t('action.save')" <el-form-item style="float: right">
:loading="loading" <el-button type="primary" @click="submitForm" :loading="formLoading">保存</el-button>
@click="submitForm()" <el-button @click="close">返回</el-button>
/> </el-form-item>
</template> </el-form>
</ContentDetailWrap> </content-wrap>
</ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { BasicInfoForm, CloumInfoForm } from './components' import { useTagsViewStore } from '@/store/modules/tagsView'
import { getCodegenTableApi, updateCodegenTableApi } from '@/api/infra/codegen' import { BasicInfoForm, ColumInfoForm, GenerateInfoForm } from './components'
import { CodegenTableVO, CodegenColumnVO, CodegenUpdateReqVO } from '@/api/infra/codegen/types' import * as CodegenApi from '@/api/infra/codegen'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { push } = useRouter() const { push, currentRoute } = useRouter() // 路由
const { query } = useRoute() const { query } = useRoute() // 查询参数
const loading = ref(false) const { delView } = useTagsViewStore() // 视图操作
const title = ref('代码生成')
const activeName = ref('basicInfo') const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const cloumInfoRef = ref(null) const activeName = ref('basicInfo') // Tag 激活的窗口
const tableCurrentRow = ref<CodegenTableVO>()
const cloumCurrentRow = ref<CodegenColumnVO[]>([])
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
const columInfoRef = ref<ComponentRef<typeof ColumInfoForm>>()
const generateInfoRef = ref<ComponentRef<typeof GenerateInfoForm>>()
const formData = ref<CodegenApi.CodegenUpdateReqVO>({
table: {},
columns: []
})
const getList = async () => { /** 获得详情 */
const getDetail = async () => {
const id = query.id as unknown as number const id = query.id as unknown as number
if (id) { if (!id) {
// 获取表详细信息 return
const res = await getCodegenTableApi(id) }
title.value = '修改[ ' + res.table.tableName + ' ]生成配置' formLoading.value = true
tableCurrentRow.value = res.table try {
cloumCurrentRow.value = res.columns formData.value = await CodegenApi.getCodegenTable(id)
} finally {
formLoading.value = false
} }
} }
/** 提交按钮 */
const submitForm = async () => { const submitForm = async () => {
const basicInfo = unref(basicInfoRef) // 参数校验
const basicForm = await basicInfo?.elFormRef?.validate()?.catch(() => {}) if (!unref(formData)) return
if (basicForm) { await unref(basicInfoRef)?.validate()
const basicInfoData = (await basicInfo?.getFormData()) as CodegenTableVO await unref(generateInfoRef)?.validate()
const genTable: CodegenUpdateReqVO = { try {
table: basicInfoData, // 提交请求
columns: cloumCurrentRow.value await CodegenApi.updateCodegenTable(formData.value)
}
await updateCodegenTableApi(genTable)
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
push('/infra/codegen') close()
} } catch {}
}
/** 关闭按钮 */
const close = () => {
delView(unref(currentRoute))
push('/infra/codegen')
} }
/** 初始化 */
onMounted(() => { onMounted(() => {
getList() getDetail()
}) })
</script> </script>
<template>
<Dialog title="导入表" v-model="modelVisible" width="800px">
<!-- 搜索栏 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="数据源" prop="dataSourceConfigId">
<el-select
v-model="queryParams.dataSourceConfigId"
placeholder="请选择数据源"
class="!w-240px"
>
<el-option
v-for="config in dataSourceConfigList"
:key="config.id"
:label="config.name"
:value="config.id"
/>
</el-select>
</el-form-item>
<el-form-item label="表名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入表名称"
clearable
@keyup.enter="getList"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="表描述" prop="comment">
<el-input
v-model="queryParams.comment"
placeholder="请输入表描述"
clearable
@keyup.enter="getList"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="getList"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
</el-form-item>
</el-form>
<!-- 列表 -->
<el-row>
<el-table
v-loading="dbTableLoading"
@row-click="handleRowClick"
ref="tableRef"
:data="dbTableList"
@selection-change="handleSelectionChange"
height="260px"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="表名称" :show-overflow-tooltip="true" />
<el-table-column prop="comment" label="表描述" :show-overflow-tooltip="true" />
</el-table>
</el-row>
<!-- 操作 -->
<template #footer>
<el-button @click="handleImportTable" type="primary" :disabled="tableList.length === 0">
导入
</el-button>
<el-button @click="close">关闭</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as CodegenApi from '@/api/infra/codegen'
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
import { ElTable } from 'element-plus'
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const dbTableLoading = ref(true) // 数据源的加载中
const dbTableList = ref<CodegenApi.DatabaseTableVO[]>([]) // 表的列表
const queryParams = reactive({
name: undefined,
comment: undefined,
dataSourceConfigId: 0
})
const queryFormRef = ref() // 搜索的表单
const dataSourceConfigList = ref<DataSourceConfigApi.DataSourceConfigVO[]>([]) // 数据源列表
/** 查询表数据 */
const getList = async () => {
dbTableLoading.value = true
try {
dbTableList.value = await CodegenApi.getSchemaTableList(queryParams)
} finally {
dbTableLoading.value = false
}
}
/** 重置操作 */
const resetQuery = async () => {
queryParams.name = undefined
queryParams.comment = undefined
queryParams.dataSourceConfigId = dataSourceConfigList.value[0].id as number
await getList()
}
/** 打开弹窗 */
const open = async () => {
// 加载数据源的列表
dataSourceConfigList.value = await DataSourceConfigApi.getDataSourceConfigList()
queryParams.dataSourceConfigId = dataSourceConfigList.value[0].id as number
modelVisible.value = true
// 加载表的列表
await getList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 关闭弹窗 */
const close = () => {
modelVisible.value = false
tableList.value = []
}
const tableRef = ref<typeof ElTable>() // 表格的 Ref
const tableList = ref<string[]>([]) // 选中的表名
/** 处理某一行的点击 */
const handleRowClick = (row) => {
unref(tableRef)?.toggleRowSelection(row)
}
/** 多选框选中数据 */
const handleSelectionChange = (selection) => {
tableList.value = selection.map((item) => item.name)
}
/** 导入按钮操作 */
const handleImportTable = async () => {
await CodegenApi.createCodegenList({
dataSourceConfigId: queryParams.dataSourceConfigId,
tableNames: tableList.value
})
message.success('导入成功')
emit('success')
close()
}
const emit = defineEmits(['success'])
</script>
<template> <template>
<XModal title="预览" v-model="preview.open"> <Dialog
:title="modelTitle"
v-model="modelVisible"
align-center
width="60%"
class="app-infra-codegen-preview-container"
>
<div class="flex"> <div class="flex">
<el-card class="w-1/4" :gutter="12" shadow="hover"> <el-card class="w-1/4" :gutter="12" shadow="hover">
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)"> <el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
...@@ -10,6 +16,7 @@ ...@@ -10,6 +16,7 @@
:expand-on-click-node="false" :expand-on-click-node="false"
highlight-current highlight-current
@node-click="handleNodeClick" @node-click="handleNodeClick"
default-expand-all
/> />
</el-scrollbar> </el-scrollbar>
</el-card> </el-card>
...@@ -21,38 +28,34 @@ ...@@ -21,38 +28,34 @@
:name="item.filePath" :name="item.filePath"
:key="item.filePath" :key="item.filePath"
> >
<XTextButton style="float: right" :title="t('common.copy')" @click="copy(item.code)" /> <el-button text type="primary" class="float-right" @click="copy(item.code)">
{{ t('common.copy') }}
</el-button>
<pre>{{ item.code }}</pre> <pre>{{ item.code }}</pre>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-card> </el-card>
</div> </div>
</XModal> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { handleTree2 } from '@/utils/tree' import { handleTree2 } from '@/utils/tree'
import { previewCodegenApi } from '@/api/infra/codegen' import * as CodegenApi from '@/api/infra/codegen'
import { CodegenTableVO, CodegenPreviewVO } from '@/api/infra/codegen/types' import { CodegenPreviewVO } from '@/api/infra/codegen/types'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('代码预览') // 弹窗的标题
// ======== 显示页面 ======== // ======== 显示页面 ========
const preview = reactive({ const preview = reactive({
open: false,
titel: '代码预览',
fileTree: [], fileTree: [],
activeName: '' activeName: ''
}) })
const previewCodegen = ref<CodegenPreviewVO[]>() const previewCodegen = ref<CodegenPreviewVO[]>()
const show = async (row: CodegenTableVO) => {
const res = await previewCodegenApi(row.id)
let file = handleFiles(res)
previewCodegen.value = res
preview.fileTree = handleTree2(file, 'id', 'parentId', 'children', '/')
preview.activeName = res[0].filePath
preview.open = true
}
const handleNodeClick = async (data, node) => { const handleNodeClick = async (data, node) => {
if (node && !node.isLeaf) { if (node && !node.isLeaf) {
return false return false
...@@ -132,14 +135,30 @@ const copy = async (text: string) => { ...@@ -132,14 +135,30 @@ const copy = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ source: text }) const { copy, copied, isSupported } = useClipboard({ source: text })
if (!isSupported) { if (!isSupported) {
message.error(t('common.copyError')) message.error(t('common.copyError'))
} else { return
await copy() }
if (unref(copied)) { await copy()
message.success(t('common.copySuccess')) if (unref(copied)) {
} message.success(t('common.copySuccess'))
} }
} }
defineExpose({
show /** 打开弹窗 */
}) const openModal = async (id: number) => {
modelVisible.value = true
const res = await CodegenApi.previewCodegen(id)
let file = handleFiles(res)
previewCodegen.value = res
preview.fileTree = handleTree2(file, 'id', 'parentId', 'children', '/')
preview.activeName = res[0].filePath
}
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
</script> </script>
<style lang="scss">
.app-infra-codegen-preview-container {
.el-scrollbar .el-scrollbar__wrap .el-scrollbar__view {
white-space: nowrap;
display: inline-block;
}
}
</style>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
title: [required],
type: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'seq',
action: true,
actionWidth: '400px',
columns: [
{
title: '表名称',
field: 'tableName',
isSearch: true
},
{
title: '表描述',
field: 'tableComment',
isSearch: true
},
{
title: '实体',
field: 'className',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: t('common.updateTime'),
field: 'updateTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
<template> <template>
<Form :rules="rules" @register="register" /> <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="表名称" prop="tableName">
<el-input placeholder="请输入仓库名称" v-model="formData.tableName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="表描述" prop="tableComment">
<el-input placeholder="请输入" v-model="formData.tableComment" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="className">
<template #label>
<span>
实体类名称
<el-tooltip
content="默认去除表名的前缀。如果存在重复,则需要手动添加前缀,避免 MyBatis 报 Alias 重复的问题。"
placement="top"
>
<Icon icon="ep:question-filled" class="" />
</el-tooltip>
</span>
</template>
<el-input placeholder="请输入" v-model="formData.className" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作者" prop="author">
<el-input placeholder="请输入" v-model="formData.author" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input type="textarea" :rows="3" v-model="formData.remark" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useForm } from '@/hooks/web/useForm'
import { FormSchema } from '@/types/form'
import { CodegenTableVO } from '@/api/infra/codegen/types' import { CodegenTableVO } from '@/api/infra/codegen/types'
import { getIntDictOptions } from '@/utils/dict'
import { getSimpleMenusList } from '@/api/system/menu'
import { handleTree, defaultProps } from '@/utils/tree'
import { PropType } from 'vue' import { PropType } from 'vue'
const emits = defineEmits(['update:basicInfo'])
const props = defineProps({ const props = defineProps({
basicInfo: { table: {
type: Object as PropType<Nullable<CodegenTableVO>>, type: Object as PropType<Nullable<CodegenTableVO>>,
default: () => null default: () => null
} }
}) })
const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE) const formRef = ref()
const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE) const formData = ref({
const menuOptions = ref<any>([]) // 树形结构 tableName: '',
const getTree = async () => { tableComment: '',
const res = await getSimpleMenusList() className: '',
menuOptions.value = handleTree(res) author: '',
} remark: ''
})
const rules = reactive({ const rules = reactive({
tableName: [required], tableName: [required],
tableComment: [required], tableComment: [required],
className: [required], className: [required],
author: [required], author: [required]
templateType: [required],
scene: [required],
moduleName: [required],
businessName: [required],
businessPackage: [required],
classComment: [required]
})
const schema = reactive<FormSchema[]>([
{
label: '上级菜单',
field: 'parentMenuId',
component: 'TreeSelect',
componentProps: {
data: menuOptions,
props: defaultProps,
checkStrictly: true,
nodeKey: 'id'
},
labelMessage: '分配到指定菜单下,例如 系统管理',
colProps: {
span: 24
}
},
{
label: '表名称',
field: 'tableName',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '表描述',
field: 'tableComment',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '实体类名称',
field: 'className',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '类名称',
field: 'className',
component: 'Input',
labelMessage: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
colProps: {
span: 12
}
},
{
label: '生成模板',
field: 'templateType',
component: 'Select',
componentProps: {
options: templateTypeOptions
},
colProps: {
span: 12
}
},
{
label: '生成场景',
field: 'scene',
component: 'Select',
componentProps: {
options: sceneOptions
},
colProps: {
span: 12
}
},
{
label: '模块名',
field: 'moduleName',
component: 'Input',
labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
colProps: {
span: 12
}
},
{
label: '业务名',
field: 'businessName',
component: 'Input',
labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
colProps: {
span: 12
}
},
{
label: '类描述',
field: 'classComment',
component: 'Input',
labelMessage: '用作类描述,例如 用户',
colProps: {
span: 12
}
},
{
label: '作者',
field: 'author',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '备注',
field: 'remark',
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
])
const { register, methods, elFormRef } = useForm({
schema
}) })
watch( watch(
() => props.basicInfo, () => props.table,
(basicInfo) => { (table) => {
if (!basicInfo) return if (!table) return
const { setValues } = methods formData.value = table
setValues(basicInfo)
}, },
{ {
deep: true, deep: true,
immediate: true immediate: true
} }
) )
// ========== 初始化 ========== watch(
onMounted(async () => { () => formData.value,
await getTree() (val) => {
}) emits('update:basicInfo', val)
}
)
defineExpose({ defineExpose({
elFormRef, validate: async () => unref(formRef)?.validate()
getFormData: methods.getFormData
}) })
</script> </script>
<template>
<vxe-table
ref="dragTable"
border
:data="info"
max-height="600"
stripe
class="xtable-scrollbar"
:column-config="{ resizable: true }"
>
<vxe-column title="字段列名" field="columnName" fixed="left" width="10%" />
<vxe-colgroup title="基础属性">
<vxe-column title="字段描述" field="columnComment" width="10%">
<template #default="{ row }">
<vxe-input v-model="row.columnComment" placeholder="请输入字段描述" />
</template>
</vxe-column>
<vxe-column title="物理类型" field="dataType" width="10%" />
<vxe-column title="Java类型" width="10%" field="javaType">
<template #default="{ row }">
<vxe-select v-model="row.javaType" placeholder="请选择Java类型">
<vxe-option label="Long" value="Long" />
<vxe-option label="String" value="String" />
<vxe-option label="Integer" value="Integer" />
<vxe-option label="Double" value="Double" />
<vxe-option label="BigDecimal" value="BigDecimal" />
<vxe-option label="LocalDateTime" value="LocalDateTime" />
<vxe-option label="Boolean" value="Boolean" />
</vxe-select>
</template>
</vxe-column>
<vxe-column title="java属性" width="8%" field="javaField">
<template #default="{ row }">
<vxe-input v-model="row.javaField" placeholder="请输入java属性" />
</template>
</vxe-column>
</vxe-colgroup>
<vxe-colgroup title="增删改查">
<vxe-column title="插入" width="40px" field="createOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
</template>
</vxe-column>
<vxe-column title="编辑" width="40px" field="updateOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
</template>
</vxe-column>
<vxe-column title="列表" width="40px" field="listOperationResult">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
</template>
</vxe-column>
<vxe-column title="查询" width="40px" field="listOperation">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
</template>
</vxe-column>
<vxe-column title="允许空" width="40px" field="nullable">
<template #default="{ row }">
<vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
</template>
</vxe-column>
<vxe-column title="查询方式" width="60px" field="listOperationCondition">
<template #default="{ row }">
<vxe-select v-model="row.listOperationCondition" placeholder="请选择查询方式">
<vxe-option label="=" value="=" />
<vxe-option label="!=" value="!=" />
<vxe-option label=">" value=">" />
<vxe-option label=">=" value=">=" />
<vxe-option label="<" value="<>" />
<vxe-option label="<=" value="<=" />
<vxe-option label="LIKE" value="LIKE" />
<vxe-option label="BETWEEN" value="BETWEEN" />
</vxe-select>
</template>
</vxe-column>
</vxe-colgroup>
<vxe-column title="显示类型" width="10%" field="htmlType">
<template #default="{ row }">
<vxe-select v-model="row.htmlType" placeholder="请选择显示类型">
<vxe-option label="文本框" value="input" />
<vxe-option label="文本域" value="textarea" />
<vxe-option label="下拉框" value="select" />
<vxe-option label="单选框" value="radio" />
<vxe-option label="复选框" value="checkbox" />
<vxe-option label="日期控件" value="datetime" />
<vxe-option label="图片上传" value="imageUpload" />
<vxe-option label="文件上传" value="fileUpload" />
<vxe-option label="富文本控件" value="editor" />
</vxe-select>
</template>
</vxe-column>
<vxe-column title="字典类型" width="10%" field="dictType">
<template #default="{ row }">
<vxe-select v-model="row.dictType" clearable filterable placeholder="请选择字典类型">
<vxe-option
v-for="dict in dictOptions"
:key="dict.id"
:label="dict.name"
:value="dict.type"
/>
</vxe-select>
</template>
</vxe-column>
<vxe-column title="示例" field="example">
<template #default="{ row }">
<vxe-input v-model="row.example" placeholder="请输入示例" />
</template>
</vxe-column>
</vxe-table>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { DictTypeVO } from '@/api/system/dict/types'
import { CodegenColumnVO } from '@/api/infra/codegen/types'
import { listSimpleDictType } from '@/api/system/dict/dict.type'
const props = defineProps({
info: {
type: Array as unknown as PropType<CodegenColumnVO[]>,
default: () => null
}
})
/** 查询字典下拉列表 */
const dictOptions = ref<DictTypeVO[]>()
const getDictOptions = async () => {
const res = await listSimpleDictType()
dictOptions.value = res
}
onMounted(async () => {
await getDictOptions()
})
defineExpose({
info: props.info
})
</script>
<template>
<el-table ref="dragTable" :data="formData" row-key="columnId" :max-height="tableHeight">
<el-table-column
label="字段列名"
prop="columnName"
min-width="10%"
:show-overflow-tooltip="true"
/>
<el-table-column label="字段描述" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.columnComment" />
</template>
</el-table-column>
<el-table-column
label="物理类型"
prop="dataType"
min-width="10%"
:show-overflow-tooltip="true"
/>
<el-table-column label="Java类型" min-width="11%">
<template #default="scope">
<el-select v-model="scope.row.javaType">
<el-option label="Long" value="Long" />
<el-option label="String" value="String" />
<el-option label="Integer" value="Integer" />
<el-option label="Double" value="Double" />
<el-option label="BigDecimal" value="BigDecimal" />
<el-option label="LocalDateTime" value="LocalDateTime" />
<el-option label="Boolean" value="Boolean" />
</el-select>
</template>
</el-table-column>
<el-table-column label="java属性" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.javaField" />
</template>
</el-table-column>
<el-table-column label="插入" min-width="4%">
<template #default="scope">
<el-checkbox true-label="true" false-label="false" v-model="scope.row.createOperation" />
</template>
</el-table-column>
<el-table-column label="编辑" min-width="4%">
<template #default="scope">
<el-checkbox true-label="true" false-label="false" v-model="scope.row.updateOperation" />
</template>
</el-table-column>
<el-table-column label="列表" min-width="4%">
<template #default="scope">
<el-checkbox
true-label="true"
false-label="false"
v-model="scope.row.listOperationResult"
/>
</template>
</el-table-column>
<el-table-column label="查询" min-width="4%">
<template #default="scope">
<el-checkbox true-label="true" false-label="false" v-model="scope.row.listOperation" />
</template>
</el-table-column>
<el-table-column label="查询方式" min-width="10%">
<template #default="scope">
<el-select v-model="scope.row.listOperationCondition">
<el-option label="=" value="=" />
<el-option label="!=" value="!=" />
<el-option label=">" value=">" />
<el-option label=">=" value=">=" />
<el-option label="<" value="<>" />
<el-option label="<=" value="<=" />
<el-option label="LIKE" value="LIKE" />
<el-option label="BETWEEN" value="BETWEEN" />
</el-select>
</template>
</el-table-column>
<el-table-column label="允许空" min-width="5%">
<template #default="scope">
<el-checkbox true-label="true" false-label="false" v-model="scope.row.nullable" />
</template>
</el-table-column>
<el-table-column label="显示类型" min-width="12%">
<template #default="scope">
<el-select v-model="scope.row.htmlType">
<el-option label="文本框" value="input" />
<el-option label="文本域" value="textarea" />
<el-option label="下拉框" value="select" />
<el-option label="单选框" value="radio" />
<el-option label="复选框" value="checkbox" />
<el-option label="日期控件" value="datetime" />
<el-option label="图片上传" value="imageUpload" />
<el-option label="文件上传" value="fileUpload" />
<el-option label="富文本控件" value="editor" />
</el-select>
</template>
</el-table-column>
<el-table-column label="字典类型" min-width="12%">
<template #default="scope">
<el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择">
<el-option
v-for="dict in dictOptions"
:key="dict.id"
:label="dict.name"
:value="dict.type"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="示例" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.example" />
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { CodegenColumnVO } from '@/api/infra/codegen/types'
import { DictTypeVO, listSimpleDictType } from '@/api/system/dict/dict.type'
const emits = defineEmits(['update:columns'])
const props = defineProps({
columns: {
type: Array as unknown as PropType<CodegenColumnVO[]>,
default: () => null
}
})
const formData = ref<CodegenColumnVO[]>([])
const tableHeight = document.documentElement.scrollHeight - 350 + 'px'
/** 查询字典下拉列表 */
const dictOptions = ref<DictTypeVO[]>()
const getDictOptions = async () => {
dictOptions.value = await listSimpleDictType()
}
onMounted(async () => {
await getDictOptions()
})
watch(
() => props.columns,
(columns) => {
if (!columns) return
formData.value = columns
},
{
deep: true,
immediate: true
}
)
watch(
() => formData.value,
(val) => {
emits('update:columns', val)
}
)
</script>
<template>
<!-- 导入表 -->
<XModal title="导入表" v-model="visible">
<el-form :model="queryParams" ref="queryRef" :inline="true">
<el-form-item label="数据源" prop="dataSourceConfigId">
<el-select v-model="queryParams.dataSourceConfigId" placeholder="请选择数据源" clearable>
<el-option
v-for="config in dataSourceConfigs"
:key="config.id"
:label="config.name"
:value="config.id"
/>
</el-select>
</el-form-item>
<el-form-item label="表名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入表名称" clearable />
</el-form-item>
<el-form-item label="表描述" prop="comment">
<el-input v-model="queryParams.comment" placeholder="请输入表描述" clearable />
</el-form-item>
<el-form-item>
<XButton
type="primary"
preIcon="ep:search"
:title="t('common.query')"
@click="handleQuery()"
/>
<XButton preIcon="ep:refresh-right" :title="t('common.reset')" @click="resetQuery()" />
</el-form-item>
</el-form>
<vxe-table
ref="xTable"
:data="dbTableList"
v-loading="dbLoading"
:checkbox-config="{ highlight: true, range: true }"
height="260px"
class="xtable-scrollbar"
>
<vxe-column type="checkbox" width="60" />
<vxe-column field="name" title="表名称" />
<vxe-column field="comment" title="表描述" />
</vxe-table>
<template #footer>
<XButton type="primary" :title="t('action.import')" @click="handleImportTable()" />
<XButton :title="t('dialog.close')" @click="handleClose()" />
</template>
</XModal>
</template>
<script setup lang="ts">
import { VxeTableInstance } from 'vxe-table'
import type { DatabaseTableVO } from '@/api/infra/codegen/types'
import { getSchemaTableListApi, createCodegenListApi } from '@/api/infra/codegen'
import { getDataSourceConfigList, DataSourceConfigVO } from '@/api/infra/dataSourceConfig'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const emit = defineEmits(['ok'])
// ======== 显示页面 ========
const visible = ref(false)
const dbLoading = ref(true)
const queryParams = reactive({
name: undefined,
comment: undefined,
dataSourceConfigId: 0 as number | undefined
})
const dataSourceConfigs = ref<DataSourceConfigVO[]>([])
const show = async () => {
const res = await getDataSourceConfigList()
dataSourceConfigs.value = res
queryParams.dataSourceConfigId = dataSourceConfigs.value[0].id as number
visible.value = true
await getList()
}
/** 查询表数据 */
const dbTableList = ref<DatabaseTableVO[]>([])
/** 查询表数据 */
const getList = async () => {
dbLoading.value = true
const res = await getSchemaTableListApi(queryParams)
dbTableList.value = res
dbLoading.value = false
}
// 查询操作
const handleQuery = async () => {
await getList()
}
// 重置操作
const resetQuery = async () => {
queryParams.name = undefined
queryParams.comment = undefined
queryParams.dataSourceConfigId = 0
await getList()
}
const xTable = ref<VxeTableInstance>()
/** 多选框选中数据 */
const tables = ref<string[]>([])
/** 导入按钮操作 */
const handleImportTable = async () => {
if (xTable.value?.getCheckboxRecords().length === 0) {
message.error('请选择要导入的表')
return
}
xTable.value?.getCheckboxRecords().forEach((item) => {
tables.value.push(item.name)
})
await createCodegenListApi({
dataSourceConfigId: queryParams.dataSourceConfigId,
tableNames: tables.value
})
message.success('导入成功')
emit('ok')
handleClose()
}
const handleClose = () => {
visible.value = false
tables.value = []
}
defineExpose({
show
})
</script>
import BasicInfoForm from './BasicInfoForm.vue' import BasicInfoForm from './BasicInfoForm.vue'
import CloumInfoForm from './CloumInfoForm.vue' import ColumInfoForm from './ColumInfoForm.vue'
import ImportTable from './ImportTable.vue' import GenerateInfoForm from './GenerateInfoForm.vue'
import Preview from './Preview.vue' export { BasicInfoForm, ColumInfoForm, GenerateInfoForm }
export { BasicInfoForm, CloumInfoForm, ImportTable, Preview }
<template> <template>
<ContentWrap> <!-- 搜索 -->
<!-- 列表 --> <content-wrap>
<XTable @register="registerTable"> <el-form
<template #toolbar_buttons> class="-mb-15px"
<!-- 操作:导入 --> :model="queryParams"
<XButton ref="queryFormRef"
type="primary" :inline="true"
preIcon="ep:zoom-in" label-width="68px"
:title="t('action.import')" >
v-hasPermi="['infra:codegen:create']" <el-form-item label="表名称" prop="tableName">
@click="openImportTable()" <el-input
v-model="queryParams.tableName"
placeholder="请输入表名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/> />
</template> </el-form-item>
<template #actionbtns_default="{ row }"> <el-form-item label="表描述" prop="tableComment">
<!-- 操作:预览 --> <el-input
<XTextButton v-model="queryParams.tableComment"
preIcon="ep:view" placeholder="请输入表描述"
:title="t('action.preview')" clearable
v-hasPermi="['infra:codegen:query']" @keyup.enter="handleQuery"
@click="handlePreview(row)" class="!w-240px"
/> />
<!-- 操作:编辑 --> </el-form-item>
<XTextButton <el-form-item label="创建时间" prop="createTime">
preIcon="ep:edit" <el-date-picker
:title="t('action.edit')" v-model="queryParams.createTime"
v-hasPermi="['infra:codegen:update']" value-format="YYYY-MM-dd HH:mm:ss"
@click="handleUpdate(row.id)" type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/> />
<!-- 操作:删除 --> </el-form-item>
<XTextButton <el-form-item>
preIcon="ep:delete" <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
:title="t('action.del')" <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
v-hasPermi="['infra:codegen:delete']" <el-button type="primary" v-hasPermi="['infra:codegen:create']" @click="openImportTable()">
@click="deleteData(row.id)" <Icon icon="ep:zoom-in" class="mr-5px" /> 导入
/> </el-button>
<!-- 操作:同步 --> </el-form-item>
<XTextButton </el-form>
preIcon="ep:refresh" </content-wrap>
:title="t('action.sync')"
v-hasPermi="['infra:codegen:update']" <!-- 列表 -->
@click="handleSynchDb(row)" <content-wrap>
/> <el-table v-loading="loading" :data="list">
<!-- 操作:生成 --> <el-table-column label="数据源" align="center">
<XTextButton <template #default="scope">
preIcon="ep:download" {{
:title="t('action.generate')" dataSourceConfigList.find((config) => config.id === scope.row.dataSourceConfigId)?.name
v-hasPermi="['infra:codegen:download']" }}
@click="handleGenTable(row)" </template>
/> </el-table-column>
</template> <el-table-column label="表名称" align="center" prop="tableName" width="200" />
</XTable> <el-table-column
</ContentWrap> label="表描述"
align="center"
prop="tableComment"
:show-overflow-tooltip="true"
width="200"
/>
<el-table-column label="实体" align="center" prop="className" width="200" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="更新时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" width="300px" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="handlePreview(scope.row)"
v-hasPermi="['infra:codegen:preview']"
>
预览
</el-button>
<el-button
link
type="primary"
@click="handleUpdate(scope.row.id)"
v-hasPermi="['infra:codegen:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['infra:codegen:delete']"
>
删除
</el-button>
<el-button
link
type="primary"
@click="handleSyncDB(scope.row)"
v-hasPermi="['infra:codegen:update']"
>
同步
</el-button>
<el-button
link
type="primary"
@click="handleGenTable(scope.row)"
v-hasPermi="['infra:codegen:download']"
>
生成代码
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</content-wrap>
<!-- 弹窗:导入表 --> <!-- 弹窗:导入表 -->
<ImportTable ref="importRef" @ok="reload()" /> <ImportTable ref="importRef" success="getList" />
<!-- 弹窗:预览代码 --> <!-- 弹窗:预览代码 -->
<Preview ref="previewRef" /> <PreviewCode ref="previewRef" />
</template> </template>
<script setup lang="ts" name="Codegen"> <script setup lang="ts" name="Codegen">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import * as CodegenApi from '@/api/infra/codegen' import * as CodegenApi from '@/api/infra/codegen'
import { CodegenTableVO } from '@/api/infra/codegen/types' import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
import { allSchemas } from './codegen.data' import ImportTable from './ImportTable.vue'
import { ImportTable, Preview } from './components' import PreviewCode from './PreviewCode.vue'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由跳转 const { push } = useRouter() // 路由跳转
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({ const loading = ref(true) // 列表的加载中
allSchemas: allSchemas, const total = ref(0) // 列表的总页数
getListApi: CodegenApi.getCodegenTablePageApi, const list = ref([]) // 列表的数据
deleteApi: CodegenApi.deleteCodegenTableApi const queryParams = reactive({
pageNo: 1,
pageSize: 10,
tableName: undefined,
tableComment: undefined,
createTime: []
}) })
const queryFormRef = ref() // 搜索的表单
const dataSourceConfigList = ref<DataSourceConfigApi.DataSourceConfigVO[]>([]) // 数据源列表
/** 查询参数列表 */
const getList = async () => {
loading.value = true
try {
const data = await CodegenApi.getCodegenTablePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
// 导入操作 // 导入操作
const importRef = ref() const importRef = ref()
const openImportTable = () => { const openImportTable = () => {
importRef.value.show() importRef.value.open()
}
/** 编辑操作 */
const handleUpdate = (id: number) => {
push('/codegen/edit?id=' + id)
} }
// 预览操作
/** 预览操作 */
const previewRef = ref() const previewRef = ref()
const handlePreview = (row: CodegenTableVO) => { const handlePreview = (row: CodegenApi.CodegenTableVO) => {
previewRef.value.show(row) previewRef.value.openModal(row.id)
} }
// 编辑操作
const handleUpdate = (rowId: number) => { /** 删除按钮操作 */
push('/codegen/edit?id=' + rowId) const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await CodegenApi.deleteCodegenTable(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
} }
// 同步操作
const handleSynchDb = (row: CodegenTableVO) => { /** 同步操作 */
const handleSyncDB = async (row: CodegenApi.CodegenTableVO) => {
// 基于 DB 同步 // 基于 DB 同步
const tableName = row.tableName const tableName = row.tableName
message try {
.confirm('确认要强制同步' + tableName + '表结构吗?', t('common.reminder')) await message.confirm('确认要强制同步' + tableName + '表结构吗?', t('common.reminder'))
.then(async () => { await CodegenApi.syncCodegenFromDB(row.id)
await CodegenApi.syncCodegenFromDBApi(row.id) message.success('同步成功')
message.success('同步成功') } catch {}
})
} }
// 生成代码操作 /** 生成代码操作 */
const handleGenTable = async (row: CodegenTableVO) => { const handleGenTable = async (row: CodegenApi.CodegenTableVO) => {
const res = await CodegenApi.downloadCodegenApi(row.id) const res = await CodegenApi.downloadCodegen(row.id)
download.zip(res, 'codegen-' + row.className + '.zip') download.zip(res, 'codegen-' + row.className + '.zip')
} }
/** 初始化 **/
onMounted(async () => {
await getList()
// 加载数据源列表
dataSourceConfigList.value = await DataSourceConfigApi.getDataSourceConfigList()
})
</script> </script>
...@@ -153,7 +153,7 @@ const queryParams = reactive({ ...@@ -153,7 +153,7 @@ const queryParams = reactive({
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -66,7 +66,7 @@ const { t } = useI18n() // 国际化 ...@@ -66,7 +66,7 @@ const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const list = ref([]) // 列表的数据 const list = ref([]) // 列表的数据
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -104,7 +104,7 @@ const queryParams = reactive({ ...@@ -104,7 +104,7 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -130,7 +130,7 @@ const queryParams = reactive({ ...@@ -130,7 +130,7 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
<template> <template>
<ContentWrap> <content-wrap>
<!-- 列表 --> <!-- 搜索栏 -->
<XTable @register="registerTable"> <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="120px">
<template #toolbar_buttons> <el-form-item label="处理器的名字" prop="handlerName">
<XButton <el-input
type="warning" v-model="queryParams.handlerName"
preIcon="ep:download" placeholder="请输入处理器的名字"
:title="t('action.export')" clearable
v-hasPermi="['infra:job:export']" @keyup.enter="handleQuery"
@click="exportList('定时任务详情.xls')" />
</el-form-item>
<el-form-item label="开始执行时间" prop="beginTime">
<el-date-picker
clearable
v-model="queryParams.beginTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择开始执行时间"
/> />
</template> </el-form-item>
<template #beginTime_default="{ row }"> <el-form-item label="结束执行时间" prop="endTime">
<span>{{ parseTime(row.beginTime) + ' ~ ' + parseTime(row.endTime) }}</span> <el-date-picker
</template> clearable
<template #duration_default="{ row }"> v-model="queryParams.endTime"
<span>{{ row.duration + ' 毫秒' }}</span> type="date"
</template> value-format="YYYY-MM-DD"
<template #actionbtns_default="{ row }"> placeholder="选择结束执行时间"
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:job:query']"
@click="handleDetail(row)"
/> />
</template> </el-form-item>
</XTable> <el-form-item label="任务状态" prop="status">
</ContentWrap> <el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable>
<XModal v-model="dialogVisible" :title="dialogTitle"> <el-option
<!-- 对话框(详情) --> v-for="dict in getDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS)"
<Descriptions :schema="allSchemas.detailSchema" :data="detailData"> :key="dict.value"
<template #retryInterval="{ row }"> :label="dict.label"
<span>{{ row.retryInterval + '毫秒' }} </span> :value="dict.value"
</template> />
<template #monitorTimeout="{ row }"> </el-select>
<span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span> </el-form-item>
</template> <el-form-item>
</Descriptions> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<!-- 操作按钮 --> <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<template #footer> <el-button
<XButton :title="t('dialog.close')" @click="dialogVisible = false" /> type="success"
</template> plain
</XModal> @click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:job:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list">
<el-table-column label="日志编号" align="center" prop="id" />
<el-table-column label="任务编号" align="center" prop="jobId" />
<el-table-column label="处理器的名字" align="center" prop="handlerName" />
<el-table-column label="处理器的参数" align="center" prop="handlerParam" />
<el-table-column label="第几次执行" align="center" prop="executeIndex" />
<el-table-column label="执行时间" align="center" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.beginTime) + ' ~ ' + parseTime(scope.row.endTime) }}</span>
</template>
</el-table-column>
<el-table-column label="执行时长" align="center" prop="duration">
<template #default="scope">
<span>{{ scope.row.duration + ' 毫秒' }}</span>
</template>
</el-table-column>
<el-table-column label="任务状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
icon="el-icon-view"
@click="handleView(scope.row.id)"
:loading="exportLoading"
v-hasPermi="['infra:job:query']"
>详细
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</content-wrap>
<!-- 表单弹窗:查看 -->
<log-view ref="viewModalRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="JobLog">
import { parseTime } from '@/utils/formatTime'
<script setup lang="ts" name="JobLog">
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import LogView from './JobLogView.vue'
import * as JobLogApi from '@/api/infra/jobLog' import * as JobLogApi from '@/api/infra/jobLog'
import { allSchemas } from './jobLog.data' import { parseTime } from './utils'
const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { exportList }] = useXTable({ const loading = ref(true) // 列表的加载中
allSchemas: allSchemas, const total = ref(0) // 列表的总页数
getListApi: JobLogApi.getJobLogPageApi, const list = ref([]) // 列表的数据
exportListApi: JobLogApi.exportJobLogApi const queryParams = reactive({
pageNo: 1,
pageSize: 10,
handlerName: undefined,
beginTime: undefined,
endTime: undefined,
status: undefined
}) })
// ========== CRUD 相关 ========== const queryFormRef = ref() // 搜索的表单
const dialogVisible = ref(false) // 是否显示弹出层 const exportLoading = ref(false) // 导出的加载中
const dialogTitle = ref('') // 弹出层标题
// ========== 详情相关 ========== /** 查询参数列表 */
const detailData = ref() // 详情 Ref const getList = async () => {
loading.value = true
try {
const data = await JobLogApi.getJobLogPageApi({
...queryParams,
beginTime: queryParams.beginTime ? queryParams.beginTime + ' 00:00:00' : undefined,
endTime: queryParams.endTime ? queryParams.endTime + ' 23:59:59' : undefined
})
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
// 详情操作 /** 搜索按钮操作 */
const handleDetail = async (row: JobLogApi.JobLogVO) => { const handleQuery = () => {
// 设置数据 queryParams.pageNo = 1
const res = await JobLogApi.getJobLogApi(row.id) getList()
detailData.value = res
dialogTitle.value = t('action.detail')
dialogVisible.value = true
} }
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 查看操作 */
const viewModalRef = ref()
const handleView = (rowId?: number) => {
viewModalRef.value.openModal(rowId)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await JobLogApi.exportJobLogApi(queryParams)
download.excel(data, '定时任务执行日志.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script> </script>
<template>
<!-- 调度日志详细 -->
<Dialog title="调度日志详细" v-model="modelVisible" width="700px" append-to-body>
<el-form ref="form" :model="formData" label-width="120px" size="mini">
<el-row>
<el-col :span="12">
<el-form-item label="日志编号:">{{ formData.id }}</el-form-item>
<el-form-item label="任务编号:">{{ formData.jobId }}</el-form-item>
<el-form-item label="处理器的名字:">{{ formData.handlerName }}</el-form-item>
<el-form-item label="处理器的参数:">{{ formData.handlerParam }}</el-form-item>
<el-form-item label="第几次执行:">{{ formData.executeIndex }}</el-form-item>
<el-form-item label="执行时间:">{{
parseTime(formData.beginTime) + ' ~ ' + parseTime(formData.endTime)
}}</el-form-item>
<el-form-item label="执行时长:">{{ formData.duration + ' 毫秒' }}</el-form-item>
<el-form-item label="任务状态:">
<dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="formData.status" />
</el-form-item>
<el-form-item label="执行结果:">{{ formData.result }}</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">关 闭</el-button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts" name="JobView">
import * as JobLogApi from '@/api/infra/jobLog'
import { DICT_TYPE } from '@/utils/dict'
import { parseTime } from './utils'
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const { t } = useI18n() // 国际化
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
id: undefined,
jobId: undefined,
handlerParam: '',
handlerName: '',
executeIndex: '',
beginTime: undefined,
endTime: undefined,
duration: true,
result: '',
status: undefined
})
/** 打开弹窗 */
const openModal = async (id?: number) => {
modelVisible.value = true
modelTitle.value = t('action.detail')
// 查看,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await JobLogApi.getJobLogApi(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
const close = () => {
emit('success')
}
</script>
<template>
<!-- 添加或修改定时任务对话框 -->
<Dialog :title="modelTitle" v-model="modelVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="任务名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="处理器的名字" prop="handlerName">
<el-input
:readonly="formData.id !== undefined"
v-model="formData.handlerName"
placeholder="请输入处理器的名字"
/>
</el-form-item>
<el-form-item label="处理器的参数" prop="handlerParam">
<el-input v-model="formData.handlerParam" placeholder="请输入处理器的参数" />
</el-form-item>
<el-form-item label="CRON 表达式" prop="cronExpression">
<el-input v-model="formData.cronExpression" placeholder="请输入CRON 表达式">
<template #append>
<el-button type="primary" @click="handleShowCron">
生成表达式
<i class="el-icon-time el-icon--right"></i>
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="重试次数" prop="retryCount">
<el-input
v-model="formData.retryCount"
placeholder="请输入重试次数。设置为 0 时,不进行重试"
/>
</el-form-item>
<el-form-item label="重试间隔" prop="retryInterval">
<el-input
v-model="formData.retryInterval"
placeholder="请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔"
/>
</el-form-item>
<el-form-item label="监控超时时间" prop="monitorTimeout">
<el-input v-model="formData.monitorTimeout" placeholder="请输入监控超时时间,单位:毫秒" />
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮:保存 -->
<div class="dialog-footer">
<el-button type="primary" @click="submitForm" :loading="formLoading">确 定</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template>
</Dialog>
<el-dialog
title="Cron表达式生成器"
v-model="openCron"
append-to-body
class="scrollbar"
destroy-on-close
>
<crontab @hide="openCron = false" @fill="crontabFill" :expression="expression" />
</el-dialog>
</template>
<script setup lang="ts" name="JobForm">
import * as JobApi from '@/api/infra/job'
const emit = defineEmits(['success', 'crontabFill']) // 定义 success 事件,用于操作成功后的回调
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const defaultFormData = {
id: undefined,
name: '',
status: 0,
handlerName: '',
handlerParam: '',
cronExpression: '',
retryCount: 0,
retryInterval: 0,
monitorTimeout: 0,
createTime: new Date()
}
const formData = ref({ ...defaultFormData })
// 是否显示Cron表达式弹出层
const openCron = ref(false)
// 传入的表达式
const expression = ref('')
// 表单校验
const formRules = reactive({
name: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }],
handlerName: [{ required: true, message: '处理器的名字不能为空', trigger: 'blur' }],
cronExpression: [{ required: true, message: 'CRON 表达式不能为空', trigger: 'blur' }],
retryCount: [{ required: true, message: '重试次数不能为空', trigger: 'blur' }],
retryInterval: [{ required: true, message: '重试间隔不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const openModal = async (type: string, id?: number) => {
modelVisible.value = true
modelTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await JobApi.getJobApi(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
/** cron表达式按钮操作 */
const handleShowCron = () => {
console.info(123333333333)
expression.value = formData.value.cronExpression
openCron.value = true
}
// cron表达式填充
const crontabFill = (expression: string) => {
formData.value.cronExpression = expression
emit('crontabFill', expression)
}
// 提交按钮
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as JobApi.JobVO
if (formType.value === 'create') {
await JobApi.createJobApi(data)
message.success(t('common.createSuccess'))
} else {
await JobApi.updateJobApi(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
...defaultFormData
}
formRef.value?.resetFields()
}
</script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
handlerName: [required],
cronExpression: [required],
retryCount: [required],
retryInterval: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'id',
primaryTitle: '任务编号',
action: true,
actionWidth: '280px',
columns: [
{
title: '任务名称',
field: 'name',
isSearch: true
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.INFRA_JOB_STATUS,
dictClass: 'number',
isForm: false,
isSearch: true
},
{
title: '处理器的名字',
field: 'handlerName',
isSearch: true
},
{
title: '处理器的参数',
field: 'handlerParam',
isTable: false
},
{
title: 'CRON 表达式',
field: 'cronExpression'
},
{
title: '后续执行时间',
field: 'nextTimes',
isTable: false,
isForm: false
},
{
title: '重试次数',
field: 'retryCount',
isTable: false
},
{
title: '重试间隔',
field: 'retryInterval',
isTable: false
},
{
title: '监控超时时间',
field: 'monitorTimeout',
isTable: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 国际化
const { t } = useI18n()
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'id',
primaryTitle: '日志编号',
action: true,
columns: [
{
title: '任务编号',
field: 'jobId',
isSearch: true
},
{
title: '处理器的名字',
field: 'handlerName',
isSearch: true
},
{
title: '处理器的参数',
field: 'handlerParam'
},
{
title: '第几次执行',
field: 'executeIndex'
},
{
title: '开始执行时间',
field: 'beginTime',
formatter: 'formatDate',
table: {
slots: {
default: 'beginTime_default'
}
},
search: {
show: true,
itemRender: {
name: 'XDataPicker'
}
}
},
{
title: '结束执行时间',
field: 'endTime',
formatter: 'formatDate',
isTable: false,
search: {
show: true,
itemRender: {
name: 'XDataPicker'
}
}
},
{
title: '执行时长',
field: 'duration',
table: {
slots: {
default: 'duration_default'
}
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.INFRA_JOB_LOG_STATUS,
dictClass: 'number',
isSearch: true
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
export const parseTime = (time) => {
if (!time) {
return null
}
const format = '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time
.replace(new RegExp(/-/gm), '/')
.replace('T', ' ')
.replace(new RegExp(/\.[\d]{3}/gm), '')
}
if (typeof time === 'number' && time.toString().length === 10) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value]
}
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
<template>
<!-- 任务详细 -->
<Dialog title="任务详细" v-model="modelVisible" width="700px" append-to-body>
<el-form ref="formRef" :model="formData" label-width="200px">
<el-row>
<el-col :span="24">
<el-form-item label="任务编号:">{{ formData.id }}</el-form-item>
<el-form-item label="任务名称:">{{ formData.name }}</el-form-item>
<el-form-item label="任务名称:">
<dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="formData.status" />
</el-form-item>
<el-form-item label="处理器的名字:">{{ formData.handlerName }}</el-form-item>
<el-form-item label="处理器的参数:">{{ formData.handlerParam }}</el-form-item>
<el-form-item label="cron表达式:">{{ formData.cronExpression }}</el-form-item>
<el-form-item label="重试次数:">{{ formData.retryCount }}</el-form-item>
<el-form-item label="重试间隔:">{{ formData.retryInterval + ' 毫秒' }}</el-form-item>
<el-form-item label="监控超时时间:">{{
formData.monitorTimeout > 0 ? formData.monitorTimeout + ' 毫秒' : '未开启'
}}</el-form-item>
<el-form-item label="后续执行时间:">
<el-timeline class="pt-3">
<el-timeline-item
v-for="(activity, index) in nextTimes"
:key="index"
:timestamp="parseTime(activity)"
>
{{ index + 1 }}
</el-timeline-item>
</el-timeline>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">关 闭</el-button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts" name="JobView">
import * as JobApi from '@/api/infra/job'
import { parseTime } from './utils'
import { DICT_TYPE } from '@/utils/dict'
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const { t } = useI18n() // 国际化
const formRef = ref() // 表单 Ref
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
id: undefined,
name: '',
handlerParam: '',
handlerName: '',
cronExpression: '',
retryCount: true,
retryInterval: '',
monitorTimeout: 0,
status: 0
})
const nextTimes = ref([])
/** 打开弹窗 */
const openModal = async (id?: number) => {
modelVisible.value = true
modelTitle.value = t('action.detail')
// 查看,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await JobApi.getJobApi(id)
// 获取下一次执行时间
nextTimes.value = await JobApi.getJobNextTimesApi(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
const close = () => {
modelVisible.value = false
emit('success')
}
</script>
...@@ -121,13 +121,13 @@ const queryFormRef = ref() // 搜索的表单 ...@@ -121,13 +121,13 @@ const queryFormRef = ref() // 搜索的表单
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
// 处理查询参数 try {
let params = { ...queryParams } const data = await AccountApi.getAccountPage(queryParams)
// 执行查询 list.value = data.list
const data = await AccountApi.getAccountPage(params) total.value = data.total
list.value = data.list } finally {
total.value = data.total loading.value = false
loading.value = false }
} }
/** 搜索按钮操作 */ /** 搜索按钮操作 */
......
<!--
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
芋道源码:
① 移除 avue 组件,使用 ElementUI 原生组件
-->
<template>
<!-- 类型:图片 -->
<div v-if="objData.type === 'image'">
<div class="waterfall" v-loading="loading">
<div class="waterfall-item" v-for="item in list" :key="item.mediaId">
<img class="material-img" :src="item.url" />
<p class="item-name">{{ item.name }}</p>
<el-row class="ope-row">
<el-button type="success" @click="selectMaterialFun(item)"
>选择
<i class="el-icon-circle-check el-icon--right"></i>
</el-button>
</el-row>
</div>
</div>
<!-- 分页组件 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
</div>
<!-- 类型:语音 -->
<div v-else-if="objData.type === 'voice'">
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="mediaId" />
<el-table-column label="文件名" align="center" prop="name" />
<el-table-column label="语音" align="center">
<template #default="scope">
<wx-voice-player :url="scope.row.url" />
</template>
</el-table-column>
<el-table-column label="上传时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
fixed="right"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button type="text" icon="el-icon-circle-plus" @click="selectMaterialFun(scope.row)"
>选择
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getPage"
/>
</div>
<div v-else-if="objData.type === 'video'">
<!-- 列表 -->
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="mediaId" />
<el-table-column label="文件名" align="center" prop="name" />
<el-table-column label="标题" align="center" prop="title" />
<el-table-column label="介绍" align="center" prop="introduction" />
<el-table-column label="视频" align="center">
<template #default="scope">
<wx-video-player :url="scope.row.url" />
</template>
</el-table-column>
<el-table-column label="上传时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
fixed="right"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button type="text" icon="el-icon-circle-plus" @click="selectMaterialFun(scope.row)"
>选择
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
</div>
<div v-else-if="objData.type === 'news'">
<div class="waterfall" v-loading="loading">
<div class="waterfall-item" v-for="item in list" :key="item.mediaId">
<div v-if="item.content && item.content.newsItem">
<wx-news :articles="item.content.newsItem" />
<el-row class="ope-row">
<el-button type="success" @click="selectMaterialFun(item)">
选择<i class="el-icon-circle-check el-icon--right"></i>
</el-button>
</el-row>
</div>
</div>
</div>
<!-- 分页组件 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
</div>
</template>
<script lang="ts" name="WxMaterialSelect">
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
import { getMaterialPage } from '@/api/mp/material'
import { getFreePublishPage } from '@/api/mp/freePublish'
import { getDraftPage } from '@/api/mp/draft'
import { dateFormatter, parseTime } from '@/utils/formatTime'
import { defineComponent, PropType } from 'vue'
export default defineComponent({
components: {
WxNews,
WxVoicePlayer,
WxVideoPlayer
},
props: {
objData: {
type: Object, // type - 类型;accountId - 公众号账号编号
required: true
},
newsType: {
// 图文类型:1、已发布图文;2、草稿箱图文
type: String as PropType<string>,
default: '1'
}
},
setup(props, ctx) {
// 遮罩层
const loading = ref(false)
// 总条数
const total = ref(0)
// 数据列表
const list = ref([])
// 查询参数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
accountId: props.objData.accountId
})
const objDataRef = reactive(props.objData)
const newsTypeRef = ref(props.newsType)
const selectMaterialFun = (item) => {
ctx.emit('selectMaterial', item)
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getPage()
}
const getPage = () => {
loading.value = true
if (objDataRef.type === 'news' && newsTypeRef.value === '1') {
// 【图文】+ 【已发布】
getFreePublishPageFun()
} else if (objDataRef.type === 'news' && newsTypeRef.value === '2') {
// 【图文】+ 【草稿】
getDraftPageFun()
} else {
// 【素材】
getMaterialPageFun()
}
}
const getMaterialPageFun = async () => {
let data = await getMaterialPage({
...queryParams,
type: objDataRef.type
})
list.value = data.list
total.value = data.total
loading.value = false
}
const getFreePublishPageFun = async () => {
let data = await getFreePublishPage(queryParams)
data.list.foreach((item) => {
const newsItem = item.content.newsItem
newsItem.forEach((article) => {
article.picUrl = article.thumbUrl
})
})
list.value = data.list
total.value = data.total
loading.value = false
}
const getDraftPageFun = async () => {
let data = await getDraftPage(queryParams)
data.list.forEach((item) => {
const newsItem = item.content.newsItem
newsItem.forEach((article) => {
article.picUrl = article.thumbUrl
})
})
list.value = data.list
total.value = data.total
loading.value = false
}
onMounted(async () => {
getPage()
})
return {
handleQuery,
dateFormatter,
selectMaterialFun,
getMaterialPageFun,
getPage,
parseTime,
newsTypeRef,
queryParams,
objDataRef,
list,
total,
loading
}
}
})
</script>
<style lang="scss" scoped>
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin: 0 auto;
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
.material-img {
width: 100%;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
p {
color: red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
/*瀑布流样式*/
</style>
...@@ -8,110 +8,78 @@ ...@@ -8,110 +8,78 @@
存在的问题:mediaId 有效期是 3 天,超过时间后无法播放 存在的问题:mediaId 有效期是 3 天,超过时间后无法播放
2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。 2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。
② 体验优化:弹窗关闭后,自动暂停视频的播放 ② 体验优化:弹窗关闭后,自动暂停视频的播放
--> -->
<template> <template>
<div> <div @click="playVideo()">
<!-- 提示 --> <!-- 提示 -->
<div @click="playVideo()"> <div>
<el-icon> <Icon icon="ep:video-play" class="mr-5px" />
<VideoPlay />
</el-icon>
<p>点击播放视频</p> <p>点击播放视频</p>
</div> </div>
<!-- 弹窗播放 --> <!-- 弹窗播放 -->
<el-dialog <el-dialog v-model="dialogVideo" title="视频播放" width="40%" append-to-body>
title="视频播放" <template #footer>
v-model:visible="dialogVideo" <video-player
width="40%" v-if="dialogVideo"
append-to-body class="video-player vjs-big-play-centered"
@close="closeDialog" :src="url"
> poster=""
<video-player crossorigin="anonymous"
v-if="playerOptions.sources[0].src" playsinline
class="video-player vjs-custom-skin" controls
ref="videoPlayerRef" :volume="0.6"
:playsinline="true" :height="320"
:options="playerOptions" :playback-rates="[0.7, 1.0, 1.5, 2.0]"
@play="onPlayerPlay($event)" />
@pause="onPlayerPause($event)" </template>
/> <!-- 事件,暫時沒用
@mounted="handleMounted"-->
<!-- @ready="handleEvent($event)"-->
<!-- @play="handleEvent($event)"-->
<!-- @pause="handleEvent($event)"-->
<!-- @ended="handleEvent($event)"-->
<!-- @loadeddata="handleEvent($event)"-->
<!-- @waiting="handleEvent($event)"-->
<!-- @playing="handleEvent($event)"-->
<!-- @canplay="handleEvent($event)"-->
<!-- @canplaythrough="handleEvent($event)"-->
<!-- @timeupdate="handleEvent(player?.currentTime())"-->
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts" name="WxVideoPlayer"> <script lang="ts" name="WxVideoPlayer">
// 引入 videoPlayer 相关组件。教程:https://juejin.cn/post/6923056942281654285 //升级videojs6.0版本,重寫6.0版本
import { videoPlayer } from 'vue-video-player' import 'video.js/dist/video-js.css'
import { defineComponent } from 'vue'
import { VideoPlayer } from '@videojs-player/vue'
import 'video.js/dist/video-js.css' import 'video.js/dist/video-js.css'
import 'vue-video-player/src/custom-theme.css'
import { VideoPlay } from '@element-plus/icons-vue'
const props = defineProps({ export default defineComponent({
url: { components: {
// 视频地址,例如说:https://www.iocoder.cn/xxx.mp4 VideoPlayer
type: String, },
required: true props: {
} url: {
}) // 视频地址,例如说:https://vjs.zencdn.net/v/oceans.mp4
const videoPlayerRef = ref() type: String,
const dialogVideo = ref(false) required: true
const playerOptions = reactive({
playbackRates: [0.5, 1.0, 1.5, 2.0], // 播放速度
autoplay: false, // 如果 true,浏览器准备好时开始回放。
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 导致视频一结束就重新开始。
preload: 'auto', // 建议浏览器在 <video> 加载元素后是否应该开始下载视频数据。auto 浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player 将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [
{
type: 'video/mp4',
src: '' // 你的视频地址(必填)【重要】
} }
], },
poster: '', // 你的封面地址 setup() {
width: document.documentElement.clientWidth, // const videoPlayerRef = ref(null)
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖 Video.js 无法播放媒体源时显示的默认信息。 const dialogVideo = ref(false)
controlBar: {
timeDivider: true,
durationDisplay: true,
remainingTimeDisplay: false,
fullscreenToggle: true //全屏按钮
}
})
const playVideo = () => { const handleEvent = (log) => {
dialogVideo.value = true console.log('Basic player event', log)
playerOptions.sources[0].src = props.url }
} const playVideo = () => {
const closeDialog = () => { dialogVideo.value = true
// 暂停播放 }
// videoPlayerRef.player.pause()
}
// onPlayerPlay(player) {},
// // // eslint-disable-next-line @typescript-eslint/no-unused-vars
// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
// onPlayerPause(player) {}
// methods: { return { handleEvent, playVideo, dialogVideo }
// playVideo() { }
// this.dialogVideo = true })
// // 设置地址
// this.playerOptions.sources[0]['src'] = this.url
// },
// closeDialog() {
// // 暂停播放
// this.$refs.videoPlayer.player.pause()
// },
//
// //todo player组件引入可能有问题
//
// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
// onPlayerPlay(player) {},
// // // eslint-disable-next-line @typescript-eslint/no-unused-vars
// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
// onPlayerPause(player) {}
// }
</script> </script>
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
<script setup lang="ts" name="WxVoicePlayer"> <script setup lang="ts" name="WxVoicePlayer">
// 因为微信语音是 amr 格式,所以需要用到 amr 解码器:https://www.npmjs.com/package/benz-amr-recorder // 因为微信语音是 amr 格式,所以需要用到 amr 解码器:https://www.npmjs.com/package/benz-amr-recorder
import BenzAMRRecorder from 'benz-amr-recorder' import BenzAMRRecorder from 'benz-amr-recorder'
const props = defineProps({ const props = defineProps({
......
...@@ -19,8 +19,14 @@ ...@@ -19,8 +19,14 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</content-wrap> </content-wrap>
...@@ -63,12 +69,19 @@ ...@@ -63,12 +69,19 @@
import { getFreePublishPage, deleteFreePublish } from '@/api/mp/freePublish' import { getFreePublishPage, deleteFreePublish } from '@/api/mp/freePublish'
import * as MpAccountApi from '@/api/mp/account' import * as MpAccountApi from '@/api/mp/account'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据 const list = ref([]) // 列表的数据
const queryParams = reactive({ interface QueryParams {
currentPage: number | undefined | string
pageNo: number | undefined | string
accountId: number | undefined | string
}
const queryParams: QueryParams = reactive({
currentPage: 1, // 当前页数 currentPage: 1, // 当前页数
pageNo: 1, // 当前页数 pageNo: 1, // 当前页数
accountId: undefined // 当前页数 accountId: undefined // 当前页数
...@@ -115,7 +128,6 @@ const resetQuery = () => { ...@@ -115,7 +128,6 @@ const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value.resetFields()
// 默认选中第一个 // 默认选中第一个
if (accountList.value.length > 0) { if (accountList.value.length > 0) {
// @ts-ignore
queryParams.accountId = accountList.value[0].id queryParams.accountId = accountList.value[0].id
} }
handleQuery() handleQuery()
...@@ -144,7 +156,6 @@ onMounted(async () => { ...@@ -144,7 +156,6 @@ onMounted(async () => {
accountList.value = await MpAccountApi.getSimpleAccountList() accountList.value = await MpAccountApi.getSimpleAccountList()
// 选中第一个 // 选中第一个
if (accountList.value.length > 0) { if (accountList.value.length > 0) {
// @ts-ignore
queryParams.accountId = accountList.value[0].id queryParams.accountId = accountList.value[0].id
} }
await getList() await getList()
......
...@@ -51,8 +51,14 @@ ...@@ -51,8 +51,14 @@
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
...@@ -85,16 +91,20 @@ ...@@ -85,16 +91,20 @@
<el-tag type="danger">取消关注</el-tag> <el-tag type="danger">取消关注</el-tag>
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'"> <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'">
<el-tag>点击菜单</el-tag>{{ scope.row.eventKey }} <el-tag>点击菜单</el-tag>
{{ scope.row.eventKey }}
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'"> <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'">
<el-tag>点击菜单链接</el-tag>{{ scope.row.eventKey }} <el-tag>点击菜单链接</el-tag>
{{ scope.row.eventKey }}
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'"> <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'">
<el-tag>扫码结果</el-tag>{{ scope.row.eventKey }} <el-tag>扫码结果</el-tag>
{{ scope.row.eventKey }}
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'"> <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'">
<el-tag>扫码结果</el-tag>{{ scope.row.eventKey }} <el-tag>扫码结果</el-tag>
{{ scope.row.eventKey }}
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'"> <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'">
<el-tag>系统拍照发图</el-tag> <el-tag>系统拍照发图</el-tag>
...@@ -125,7 +135,8 @@ ...@@ -125,7 +135,8 @@
<wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" /> <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
</div> </div>
<div v-else-if="scope.row.type === 'link'"> <div v-else-if="scope.row.type === 'link'">
<el-tag>链接</el-tag> <el-tag>链接</el-tag>
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a> <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
</div> </div>
<div v-else-if="scope.row.type === 'location'"> <div v-else-if="scope.row.type === 'location'">
...@@ -175,23 +186,26 @@ ...@@ -175,23 +186,26 @@
/> />
<!-- 发送消息的弹窗 --> <!-- 发送消息的弹窗 -->
<el-dialog title="粉丝消息列表" v-model:visible="open" width="50%"> <el-dialog title="粉丝消息列表" v-model="open" @click="openDialog()" width="50%">
<wx-msg :user-id="userId" v-if="open" /> <template #footer>
<wx-msg :user-id="userId" v-if="open" />
</template>
</el-dialog> </el-dialog>
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts" name="MpMessage"> <script setup lang="ts" name="MpMessage">
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
import { dateFormatter } from '@/utils/formatTime'
// import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
// import WxMsg from '@/views/mp/components/wx-msg/main.vue' import WxMsg from '@/views/mp/components/wx-msg/main.vue'
import WxLocation from '@/views/mp/components/wx-location/main.vue' import WxLocation from '@/views/mp/components/wx-location/main.vue'
// import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxMusic from '@/views/mp/components/wx-music/main.vue'
// import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
import * as MpAccountApi from '@/api/mp/account' import * as MpAccountApi from '@/api/mp/account'
import * as MpMessageApi from '@/api/mp/message' import * as MpMessageApi from '@/api/mp/message'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
...@@ -210,7 +224,7 @@ const open = ref(false) // 是否显示弹出层 ...@@ -210,7 +224,7 @@ const open = ref(false) // 是否显示弹出层
const userId = ref(0) // 操作的用户编号 const userId = ref(0) // 操作的用户编号
const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表 const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
// 如果没有选中公众号账号,则进行提示。 // 如果没有选中公众号账号,则进行提示。
if (!queryParams.accountId) { if (!queryParams.accountId) {
...@@ -248,6 +262,13 @@ const handleSend = async (row) => { ...@@ -248,6 +262,13 @@ const handleSend = async (row) => {
open.value = true open.value = true
} }
const openDialog = () => {
open.value = true
}
// const closeDiaLog = () => {
// open.value = false
// }
/** 初始化 **/ /** 初始化 **/
onMounted(async () => { onMounted(async () => {
accountList.value = await MpAccountApi.getSimpleAccountList() accountList.value = await MpAccountApi.getSimpleAccountList()
......
...@@ -28,13 +28,21 @@ ...@@ -28,13 +28,21 @@
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']"> <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" />
新增
</el-button> </el-button>
<el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']"> <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']">
<Icon icon="ep:refresh" class="mr-5px" /> 同步 <Icon icon="ep:refresh" class="mr-5px" />
同步
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
...@@ -91,6 +99,7 @@ import { dateFormatter } from '@/utils/formatTime' ...@@ -91,6 +99,7 @@ import { dateFormatter } from '@/utils/formatTime'
import * as MpTagApi from '@/api/mp/tag' import * as MpTagApi from '@/api/mp/tag'
import * as MpAccountApi from '@/api/mp/account' import * as MpAccountApi from '@/api/mp/account'
import TagForm from './TagForm.vue' import TagForm from './TagForm.vue'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
...@@ -106,7 +115,7 @@ const queryParams = reactive({ ...@@ -106,7 +115,7 @@ const queryParams = reactive({
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表 const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
// 如果没有选中公众号账号,则进行提示。 // 如果没有选中公众号账号,则进行提示。
if (!queryParams.accountId) { if (!queryParams.accountId) {
......
...@@ -130,7 +130,7 @@ const queryFormRef = ref() // 搜索的表单 ...@@ -130,7 +130,7 @@ const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
const dicts = ref<DictTypeApi.DictTypeVO[]>() // 字典类型的列表 const dicts = ref<DictTypeApi.DictTypeVO[]>() // 字典类型的列表
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -123,7 +123,7 @@ const queryParams = reactive({ ...@@ -123,7 +123,7 @@ const queryParams = reactive({
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -126,7 +126,7 @@ const queryFormRef = ref() // 搜索的表单 ...@@ -126,7 +126,7 @@ const queryFormRef = ref() // 搜索的表单
const isExpandAll = ref(false) // 是否展开,默认全部折叠 const isExpandAll = ref(false) // 是否展开,默认全部折叠
const refreshTable = ref(true) // 重新渲染表格状态 const refreshTable = ref(true) // 重新渲染表格状态
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
<template>
<Dialog title="详情" v-model="modelVisible" :scroll="true" :max-height="500">
<el-descriptions border :column="1">
<el-descriptions-item label="编号" min-width="120">
{{ detailData.id }}
</el-descriptions-item>
<el-descriptions-item label="用户类型">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
</el-descriptions-item>
<el-descriptions-item label="用户编号">
{{ detailData.userId }}
</el-descriptions-item>
<el-descriptions-item label="模版编号">
{{ detailData.templateId }}
</el-descriptions-item>
<el-descriptions-item label="模板编码">
{{ detailData.templateCode }}
</el-descriptions-item>
<el-descriptions-item label="发送人名称">
{{ detailData.templateNickname }}
</el-descriptions-item>
<el-descriptions-item label="模版内容">
{{ detailData.templateContent }}
</el-descriptions-item>
<el-descriptions-item label="模版参数">
{{ detailData.templateParams }}
</el-descriptions-item>
<el-descriptions-item label="模版类型">
<dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="detailData.templateType" />
</el-descriptions-item>
<el-descriptions-item label="是否已读">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="detailData.readStatus" />
</el-descriptions-item>
<el-descriptions-item label="阅读时间">
{{ formatDate(detailData.readTime) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(detailData.createTime) }}
</el-descriptions-item>
</el-descriptions>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import * as NotifyMessageApi from '@/api/system/notify/message'
const modelVisible = ref(false) // 弹窗的是否展示
const detailLoading = ref(false) // 表单的加载中
const detailData = ref() // 详情数据
/** 打开弹窗 */
const open = async (data: NotifyMessageApi.NotifyMessageVO) => {
modelVisible.value = true
// 设置数据
detailLoading.value = true
try {
detailData.value = data
} finally {
detailLoading.value = false
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
</script>
<template> <template>
<ContentWrap> <content-wrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<template #actionbtns_default="{ row }"> class="-mb-15px"
<!-- 操作:详情 --> :model="queryParams"
<XTextButton ref="queryFormRef"
preIcon="ep:view" :inline="true"
:title="t('action.detail')" label-width="68px"
v-hasPermi="['system:notify-message:query']" >
@click="handleDetail(row.id)" <el-form-item label="用户编号" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/> />
</template> </el-form-item>
</XTable> <el-form-item label="用户类型" prop="userType">
</ContentWrap> <el-select
<!-- 弹窗 --> v-model="queryParams.userType"
<XModal id="messageModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle"> placeholder="请选择用户类型"
<!-- 表单:详情 --> clearable
<Descriptions class="!w-240px"
v-if="actionType === 'detail'" >
:schema="allSchemas.detailSchema" <el-option
:data="detailData" v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="模板编码" prop="templateCode">
<el-input
v-model="queryParams.templateCode"
placeholder="请输入模板编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="模版类型" prop="templateType">
<el-select
v-model="queryParams.templateType"
placeholder="请选择模版类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
</el-form-item>
</el-form>
</content-wrap>
<!-- 列表 -->
<content-wrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="用户类型" align="center" prop="userType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
</template>
</el-table-column>
<el-table-column label="用户编号" align="center" prop="userId" width="80" />
<el-table-column label="模板编码" align="center" prop="templateCode" width="80" />
<el-table-column label="发送人名称" align="center" prop="templateNickname" width="180" />
<el-table-column
label="模版内容"
align="center"
prop="templateContent"
width="200"
show-overflow-tooltip
/>
<el-table-column
label="模版参数"
align="center"
prop="templateParams"
width="180"
show-overflow-tooltip
>
<template #default="scope"> {{ scope.row.templateParams }}</template>
</el-table-column>
<el-table-column label="模版类型" align="center" prop="templateType" width="120">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="scope.row.templateType" />
</template>
</el-table-column>
<el-table-column label="是否已读" align="center" prop="readStatus" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.readStatus" />
</template>
</el-table-column>
<el-table-column
label="阅读时间"
align="center"
prop="readTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openDetail(scope.row)"
v-hasPermi="['system:notify-message:query']"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/> />
<template #footer> </content-wrap>
<!-- 按钮:关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" /> <!-- 表单弹窗:详情 -->
</template> <NotifyMessageDetail ref="detailRef" />
</XModal>
</template> </template>
<script setup lang="ts" name="NotifyMessage"> <script setup lang="ts" name="NotifyMessage">
// 业务相关的 import import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { allSchemas } from './message.data' import { dateFormatter } from '@/utils/formatTime'
import * as NotifyMessageApi from '@/api/system/notify/message' import * as NotifyMessageApi from '@/api/system/notify/message'
import NotifyMessageDetail from './NotifyMessageDetail.vue'
const { t } = useI18n() // 国际化 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
// 列表相关的变量 const list = ref([]) // 列表的数据
const [registerTable] = useXTable({ const queryParams = reactive({
allSchemas: allSchemas, pageNo: 1,
topActionSlots: false, pageSize: 10,
getListApi: NotifyMessageApi.getNotifyMessagePageApi userType: undefined,
userId: undefined,
templateCode: undefined,
templateType: undefined,
createTime: []
}) })
const queryFormRef = ref() // 搜索的表单
// 弹窗相关的变量 /** 查询列表 */
const modelVisible = ref(false) // 是否显示弹出层 const getList = async () => {
const modelTitle = ref('edit') // 弹出层标题 loading.value = true
const modelLoading = ref(false) // 弹出层loading try {
const actionType = ref('') // 操作按钮的类型 const data = await NotifyMessageApi.getNotifyMessagePage(queryParams)
const actionLoading = ref(false) // 按钮 Loading list.value = data.list
const detailData = ref() // 详情 Ref total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
// 设置标题 /** 重置按钮操作 */
const setDialogTile = (type: string) => { const resetQuery = () => {
modelLoading.value = true queryFormRef.value.resetFields()
modelTitle.value = t('action.' + type) handleQuery()
actionType.value = type
modelVisible.value = true
} }
// 详情操作 /** 详情操作 */
const handleDetail = async (rowId: number) => { const detailRef = ref()
setDialogTile('detail') const openDetail = (data: NotifyMessageApi.NotifyMessageVO) => {
const res = await NotifyMessageApi.getNotifyMessageApi(rowId) detailRef.value.open(data)
detailData.value = res
modelLoading.value = false
} }
/** 初始化 **/
onMounted(() => {
getList()
})
</script> </script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id', // 默认的主键ID
primaryTitle: '编号', // 默认显示的值
primaryType: 'id', // 默认为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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment