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>
<!-- 第一步,通过流程定义的列表,选择对应的流程 --> <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
<div v-if="!selectProcessInstance"> <ContentWrap v-if="!selectProcessInstance">
<XTable @register="registerTable"> <el-table v-loading="loading" :data="list">
<!-- 流程分类 --> <el-table-column label="流程名称" align="center" prop="name" />
<template #category_default="{ row }"> <el-table-column label="流程分类" align="center" prop="category">
<DictTag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="Number(row?.category)" /> <template #default="scope">
<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> <ContentWrap v-else>
<el-card class="box-card"> <el-card class="box-card">
<div class="clearfix"> <div class="clearfix">
<span class="el-icon-document">申请信息【{{ selectProcessInstance.name }}】</span> <span class="el-icon-document">申请信息【{{ selectProcessInstance.name }}】</span>
<XButton <el-button style="float: right" type="primary" @click="selectProcessInstance = undefined">
style="float: right" <Icon icon="ep:delete" /> 选择其它流程
type="primary" </el-button>
preIcon="ep:delete"
title="选择其它流程"
@click="selectProcessInstance = undefined"
/>
</div> </div>
<el-col :span="16" :offset="6" style="margin-top: 20px"> <el-col :span="16" :offset="6" style="margin-top: 20px">
<form-create <form-create
...@@ -37,57 +42,47 @@ ...@@ -37,57 +42,47 @@
/> />
</el-col> </el-col>
</el-card> </el-card>
<el-card class="box-card"> <!-- 流程图预览 -->
<div class="clearfix"> <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML" />
<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>
</div>
</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>
<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">
<XButton
pre-icon="ep:select"
type="success"
title="通过"
@click="handleAudit(item, true)"
/>
<XButton
pre-icon="ep:close"
type="danger"
title="不通过"
@click="handleAudit(item, false)"
/>
<XButton
pre-icon="ep:edit"
type="primary"
title="转办"
@click="handleUpdateAssignee(item)"
/>
<XButton
pre-icon="ep:position"
type="primary"
title="委派"
@click="handleDelegate(item)"
/>
<XButton pre-icon="ep:back" type="warning" title="委派" @click="handleBack(item)" />
</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
"
>
<XButton type="primary" preIcon="ep:view" title="点击查看" />
</router-link>
</div>
</el-card>
<!-- 审批记录 -->
<el-card class="box-card" v-loading="tasksLoad">
<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>
<!-- 高亮流程图 -->
<el-card class="box-card" v-loading="processInstanceLoading">
<template #header>
<span class="el-icon-picture-outline">流程图</span>
</template>
<my-process-viewer
key="designer"
v-model="bpmnXML"
:value="bpmnXML"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
:activityData="activityList"
:processInstanceData="processInstance"
:taskData="tasks"
/>
</el-card>
<!-- 对话框(转派审批人) -->
<XModal v-model="updateAssigneeVisible" title="转派审批人" width="500">
<el-form
ref="updateAssigneeFormRef"
:model="updateAssigneeForm"
:rules="updateAssigneeRules"
label-width="110px"
>
<el-form-item label="新审批人" prop="assigneeUserId">
<el-select v-model="updateAssigneeForm.assigneeUserId" clearable style="width: 100%">
<el-option
v-for="item in userOptions"
:key="parseInt(item.id)"
:label="item.nickname"
:value="parseInt(item.id)"
/>
</el-select>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮:保存 -->
<XButton
type="primary"
:title="t('action.save')"
:loading="updateAssigneeLoading"
@click="submitUpdateAssigneeForm"
/>
<!-- 按钮:关闭 -->
<XButton
:loading="updateAssigneeLoading"
:title="t('dialog.close')"
@click="updateAssigneeLoading = false"
/>
</template>
</XModal>
</ContentWrap>
</template>
<script setup lang="ts">
import { parseTime } from '@/utils/formatTime'
import * as UserApi from '@/api/system/user'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition'
import * as TaskApi from '@/api/bpm/task'
import * as ActivityApi from '@/api/bpm/activity'
import { formatPast2 } from '@/utils/formatTime'
import { setConfAndFields2 } from '@/utils/formCreate'
// import { OptionAttrs } from '@form-create/element-ui/types/config'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { useUserStore } from '@/store/modules/user'
const { query } = useRoute() // 查询参数
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { proxy } = getCurrentInstance() as any
// ========== 审批信息 ==========
const id = query.id as unknown as number
const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例
const runningTasks = ref<any[]>([]) // 运行中的任务
const auditForms = ref<any[]>([]) // 审批任务的表单
const auditRule = reactive({
reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
})
// 处理审批通过和不通过的操作
const handleAudit = async (task, pass) => {
// 1.1 获得对应表单
const index = runningTasks.value.indexOf(task)
const auditFormRef = proxy.$refs['form' + index][0]
// alert(auditFormRef)
// 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 fApi = ref<ApiAttrs>()
const userId = useUserStore().getUser.id // 当前登录的编号
// 流程表单详情
const detailForm = ref({
rule: [],
option: {},
value: {}
})
// ========== 审批记录 ==========
const tasksLoad = ref(true)
const tasks = ref<any[]>([])
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 ''
}
// ========== 审批记录 ==========
const updateAssigneeVisible = ref(false)
const updateAssigneeLoading = ref(false)
const updateAssigneeForm = ref({
id: undefined,
assigneeUserId: undefined
})
const updateAssigneeRules = ref({
assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }]
})
const updateAssigneeFormRef = ref()
const userOptions = ref<any[]>([])
// 处理转派审批人
const handleUpdateAssignee = (task) => {
// 设置表单
resetUpdateAssigneeForm()
updateAssigneeForm.value.id = task.id
// 设置为打开
updateAssigneeVisible.value = true
}
// 提交转派审批人
const submitUpdateAssigneeForm = async () => {
// 1. 校验表单
const elForm = unref(updateAssigneeFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 2.1 提交审批
updateAssigneeLoading.value = true
try {
await TaskApi.updateTaskAssignee(updateAssigneeForm.value)
// 2.2 设置为隐藏
updateAssigneeVisible.value = false
// 加载最新数据
getDetail()
} finally {
updateAssigneeLoading.value = false
}
}
// 重置转派审批人表单
const resetUpdateAssigneeForm = () => {
updateAssigneeForm.value = {
id: undefined,
assigneeUserId: undefined
}
updateAssigneeFormRef.value?.resetFields()
}
/** 处理审批退回的操作 */
const handleDelegate = async (task) => {
message.error('暂不支持【委派】功能,可以使用【转派】替代!')
console.log(task)
}
/** 处理审批退回的操作 */
const handleBack = async (task) => {
message.error('暂不支持【退回】功能!')
// 可参考 http://blog.wya1.com/article/636697030/details/7296
// const data = {
// id: task.id,
// assigneeUserId: 1
// }
// backTask(data).then(response => {
// this.$modal.msgSuccess("回退成功!");
// this.getDetail(); // 获得最新详情
// });
console.log(task)
}
// ========== 高亮流程图 ==========
const bpmnXML = ref(null)
const bpmnControlForm = ref({
prefix: 'flowable'
})
const activityList = ref([])
// ========== 初始化 ==========
onMounted(() => {
// 加载详情
getDetail()
// 加载用户的列表
UserApi.getSimpleUserList().then((data) => {
userOptions.value.push(...data)
})
})
const getDetail = () => {
// 1. 获得流程实例相关
processInstanceLoading.value = true
ProcessInstanceApi.getProcessInstanceApi(id)
.then((data) => {
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)
})
}
// 加载流程图
DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id).then((data) => {
bpmnXML.value = data
})
// 加载活动列表
ActivityApi.getActivityList({
processInstanceId: data.id
}).then((data) => {
activityList.value = data
})
})
.finally(() => {
processInstanceLoading.value = false
})
// 2. 获得流程任务列表(审批记录)
tasksLoad.value = true
runningTasks.value = []
auditForms.value = []
TaskApi.getTaskListByProcessInstanceId(id)
.then((data) => {
// 审批记录
tasks.value = []
// 移除已取消的审批
data.forEach((task) => {
if (task.result !== 4) {
tasks.value.push(task)
}
})
// 排序,将未完成的排在前面,已完成的排在后面;
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
}
})
// 需要审核的记录
tasks.value.forEach((task) => {
// 1.1 只有待处理才需要
if (task.result !== 1) {
return
}
// 1.2 自己不是处理人
if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return
}
// 2. 添加到处理任务
runningTasks.value.push({ ...task })
auditForms.value.push({
reason: ''
})
})
})
.finally(() => {
tasksLoad.value = false
})
}
</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">
<BasicInfoForm ref="basicInfoRef" :basicInfo="tableCurrentRow" /> <basic-info-form ref="basicInfoRef" :table="formData.table" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="字段信息" name="cloum"> <el-tab-pane label="字段信息" name="colum">
<CloumInfoForm ref="cloumInfoRef" :info="cloumCurrentRow" /> <colum-info-form ref="columInfoRef" :columns="formData.columns" />
</el-tab-pane>
<el-tab-pane label="生成信息" name="generateInfo">
<generate-info-form ref="generateInfoRef" :table="formData.table" />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<template #right> <el-form>
<XButton <el-form-item style="float: right">
type="primary" <el-button type="primary" @click="submitForm" :loading="formLoading">保存</el-button>
:title="t('action.save')" <el-button @click="close">返回</el-button>
:loading="loading" </el-form-item>
@click="submitForm()" </el-form>
/> </content-wrap>
</template>
</ContentDetailWrap>
</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'))
close()
} catch {}
}
/** 关闭按钮 */
const close = () => {
delView(unref(currentRoute))
push('/infra/codegen') 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() await copy()
if (unref(copied)) { if (unref(copied)) {
message.success(t('common.copySuccess')) 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>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="150px">
<el-row>
<el-col :span="12">
<el-form-item prop="templateType" label="生成模板">
<el-select v-model="formData.templateType" @change="tplSelectChange">
<el-option
v-for="dict in getDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="scene" label="生成场景">
<el-select v-model="formData.scene">
<el-option
v-for="dict in getDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
</el-select>
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item prop="packageName">-->
<!-- <span slot="label">-->
<!-- 生成包路径-->
<!-- <el-tooltip content="生成在哪个java包下,例如 com.ruoyi.system" placement="top">-->
<!-- <i class="el-icon-question"></i>-->
<!-- </el-tooltip>-->
<!-- </span>-->
<!-- <el-input v-model="formData.packageName" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item prop="moduleName">
<template #label>
<span>
模块名
<el-tooltip
content="模块名,即一级目录,例如 system、infra、tool 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.moduleName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="businessName">
<template #label>
<span>
业务名
<el-tooltip
content="业务名,即二级目录,例如 user、permission、dict 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.businessName" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item prop="businessPackage">-->
<!-- <span slot="label">-->
<!-- 业务包-->
<!-- <el-tooltip content="业务包,自定义二级目录。例如说,我们希望将 dictType 和 dictData 归类成 dict 业务" placement="top">-->
<!-- <i class="el-icon-question"></i>-->
<!-- </el-tooltip>-->
<!-- </span>-->
<!-- <el-input v-model="formData.businessPackage" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item prop="className">
<template #label>
<span>
类名称
<el-tooltip
content="类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.className" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="classComment">
<template #label>
<span>
类描述
<el-tooltip content="用作类描述,例如 用户" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.classComment" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
上级菜单
<el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-tree-select
v-model="formData.parentMenuId"
placeholder="请选择系统菜单"
node-key="id"
check-strictly
:data="menus"
:props="menuTreeProps"
/>
</el-form-item>
</el-col>
<el-col :span="24" v-if="formData.genType === '1'">
<el-form-item prop="genPath">
<template #label>
<span>
自定义路径
<el-tooltip
content="填写磁盘绝对路径,若不填写,则生成到当前Web项目下"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.genPath">
<template #append>
<el-dropdown>
<el-button type="primary">
最近路径快速选择
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="formData.genPath = '/'">
恢复默认的生成基础路径
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="formData.tplCategory === 'tree'">
<h4 class="form-header">其他信息</h4>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树编码字段
<el-tooltip content="树显示的编码字段名, 如:dept_id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeCode" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树父编码字段
<el-tooltip content="树显示的父编码字段名, 如:parent_Id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeParentCode" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树名称字段
<el-tooltip content="树节点的显示名称字段名, 如:dept_name" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeName" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="formData.tplCategory === 'sub'">
<h4 class="form-header">关联信息</h4>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
关联子表的表名
<el-tooltip content="关联子表的表名, 如:sys_user" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.subTableName" placeholder="请选择" @change="subSelectChange">
<el-option
v-for="(table0, index) in tables"
:key="index"
:label="table0.tableName + ':' + table0.tableComment"
:value="table0.tableName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
子表关联的外键名
<el-tooltip content="子表关联的外键名, 如:user_id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.subTableFkName" placeholder="请选择">
<el-option
v-for="(column, index) in subColumns"
:key="index"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { CodegenTableVO } from '@/api/infra/codegen/types'
import * as MenuApi from '@/api/system/menu'
import { PropType } from 'vue'
import { getDictOptions, DICT_TYPE } from '@/utils/dict'
import { handleTree } from '@/utils/tree'
const message = useMessage() // 消息弹窗
const emits = defineEmits(['update:basicInfo'])
const props = defineProps({
table: {
type: Object as PropType<Nullable<CodegenTableVO>>,
default: () => null
}
})
const formRef = ref()
const formData = ref({
templateType: null,
scene: null,
moduleName: '',
businessName: '',
className: '',
classComment: '',
parentMenuId: null,
genPath: '',
treeCode: '',
treeParentCode: '',
treeName: '',
tplCategory: '',
subTableName: '',
subTableFkName: '',
genType: ''
})
const rules = reactive({
templateType: [required],
scene: [required],
moduleName: [required],
businessName: [required],
businessPackage: [required],
className: [required],
classComment: [required]
})
const tables = ref([])
const subColumns = ref([])
const menus = ref<any[]>([])
const menuTreeProps = {
label: 'name'
}
/** 选择子表名触发 */
const subSelectChange = () => {
formData.value.subTableFkName = ''
}
/** 选择生成模板触发 */
const tplSelectChange = (value) => {
if (value !== 1) {
// TODO 芋艿:暂时不考虑支持树形结构
message.error(
'暂时不考虑支持【树形】和【主子表】的代码生成。原因是:导致 vm 模板过于复杂,不利于胖友二次开发'
)
return false
}
if (value !== 'sub') {
formData.value.subTableName = ''
formData.value.subTableFkName = ''
}
}
watch(
() => props.table,
(table) => {
if (!table) return
formData.value = table as any
},
{
deep: true,
immediate: true
}
)
watch(
() => formData.value,
(val) => {
emits('update:basicInfo', val)
}
)
onMounted(async () => {
try {
const resp = await MenuApi.getSimpleMenusList()
menus.value = handleTree(resp)
} catch {}
})
defineExpose({
validate: async () => unref(formRef)?.validate()
})
</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"
/>
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
v-model="queryParams.tableComment"
placeholder="请输入表描述"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/> />
</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-button type="primary" v-hasPermi="['infra:codegen:create']" @click="openImportTable()">
<Icon icon="ep:zoom-in" 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">
<template #default="scope">
{{
dataSourceConfigList.find((config) => config.id === scope.row.dataSourceConfigId)?.name
}}
</template> </template>
<template #actionbtns_default="{ row }"> </el-table-column>
<!-- 操作:预览 --> <el-table-column label="表名称" align="center" prop="tableName" width="200" />
<XTextButton <el-table-column
preIcon="ep:view" label="表描述"
:title="t('action.preview')" align="center"
v-hasPermi="['infra:codegen:query']" prop="tableComment"
@click="handlePreview(row)" :show-overflow-tooltip="true"
width="200"
/> />
<!-- 操作:编辑 --> <el-table-column label="实体" align="center" prop="className" width="200" />
<XTextButton <el-table-column
preIcon="ep:edit" label="创建时间"
:title="t('action.edit')" align="center"
v-hasPermi="['infra:codegen:update']" prop="createTime"
@click="handleUpdate(row.id)" width="180"
:formatter="dateFormatter"
/> />
<!-- 操作:删除 --> <el-table-column
<XTextButton label="更新时间"
preIcon="ep:delete" align="center"
:title="t('action.del')" prop="createTime"
v-hasPermi="['infra:codegen:delete']" width="180"
@click="deleteData(row.id)" :formatter="dateFormatter"
/> />
<!-- 操作:同步 --> <el-table-column label="操作" align="center" width="300px" fixed="right">
<XTextButton <template #default="scope">
preIcon="ep:refresh" <el-button
:title="t('action.sync')" 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']" v-hasPermi="['infra:codegen:update']"
@click="handleSynchDb(row)" >
/> 编辑
<!-- 操作:生成 --> </el-button>
<XTextButton <el-button
preIcon="ep:download" link
:title="t('action.generate')" 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']" v-hasPermi="['infra:codegen:download']"
@click="handleGenTable(row)" >
/> 生成代码
</el-button>
</template> </template>
</XTable> </el-table-column>
</ContentWrap> </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')"
/> />
</template> </el-form-item>
<template #beginTime_default="{ row }"> <el-form-item label="开始执行时间" prop="beginTime">
<span>{{ parseTime(row.beginTime) + ' ~ ' + parseTime(row.endTime) }}</span> <el-date-picker
</template> clearable
<template #duration_default="{ row }"> v-model="queryParams.beginTime"
<span>{{ row.duration + ' 毫秒' }}</span> type="date"
</template> value-format="YYYY-MM-DD"
<template #actionbtns_default="{ row }"> placeholder="选择开始执行时间"
<XTextButton />
preIcon="ep:view" </el-form-item>
:title="t('action.detail')" <el-form-item label="结束执行时间" prop="endTime">
v-hasPermi="['infra:job:query']" <el-date-picker
@click="handleDetail(row)" clearable
v-model="queryParams.endTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择结束执行时间"
/> />
</el-form-item>
<el-form-item label="任务状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable>
<el-option
v-for="dict in getDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</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-button
type="success"
plain
@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> </template>
</XTable> </el-table-column>
</ContentWrap> <el-table-column label="执行时长" align="center" prop="duration">
<XModal v-model="dialogVisible" :title="dialogTitle"> <template #default="scope">
<!-- 对话框(详情) --> <span>{{ scope.row.duration + ' 毫秒' }}</span>
<Descriptions :schema="allSchemas.detailSchema" :data="detailData">
<template #retryInterval="{ row }">
<span>{{ row.retryInterval + '毫秒' }} </span>
</template> </template>
<template #monitorTimeout="{ row }"> </el-table-column>
<span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span> <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> </template>
</Descriptions> </el-table-column>
<!-- 操作按钮 --> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #footer> <template #default="scope">
<XButton :title="t('dialog.close')" @click="dialogVisible = false" /> <el-button
link
icon="el-icon-view"
@click="handleView(scope.row.id)"
:loading="exportLoading"
v-hasPermi="['infra:job:query']"
>详细
</el-button>
</template> </template>
</XModal> </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 message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const loading = ref(true) // 列表的加载中
// 列表相关的变量 const total = ref(0) // 列表的总页数
const [registerTable, { exportList }] = useXTable({ const list = ref([]) // 列表的数据
allSchemas: allSchemas, const queryParams = reactive({
getListApi: JobLogApi.getJobLogPageApi, pageNo: 1,
exportListApi: JobLogApi.exportJobLogApi 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 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 handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
// ========== 详情相关 ========== /** 查看操作 */
const detailData = ref() // 详情 Ref const viewModalRef = ref()
const handleView = (rowId?: number) => {
viewModalRef.value.openModal(rowId)
}
// 详情操作 /** 导出按钮操作 */
const handleDetail = async (row: JobLogApi.JobLogVO) => { const handleExport = async () => {
// 设置数据 try {
const res = await JobLogApi.getJobLogApi(row.id) // 导出的二次确认
detailData.value = res await message.exportConfirm()
dialogTitle.value = t('action.detail') // 发起导出
dialogVisible.value = true 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>
<template> <template>
<ContentWrap> <content-wrap>
<!-- 列表 --> <!-- 搜索栏 -->
<XTable @register="registerTable"> <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px">
<template #toolbar_buttons> <el-form-item label="任务名称" prop="name">
<!-- 操作:新增 --> <el-input
<XButton v-model="queryParams.name"
type="primary" placeholder="请输入任务名称"
preIcon="ep:zoom-in" clearable
:title="t('action.add')" @keyup.enter="handleQuery"
v-hasPermi="['infra:job:create']"
@click="handleCreate()"
/> />
<!-- 操作:导出 --> </el-form-item>
<XButton <el-form-item label="任务状态" prop="status">
type="warning" <el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable>
preIcon="ep:download" <el-option
:title="t('action.export')" v-for="dict in getDictOptions(DICT_TYPE.INFRA_JOB_STATUS)"
v-hasPermi="['infra:job:export']" :key="dict.value"
@click="exportList('定时任务.xls')" :label="dict.label"
:value="dict.value"
/> />
<XButton </el-select>
type="info" </el-form-item>
preIcon="ep:zoom-in" <el-form-item label="处理器的名字" prop="handlerName">
title="执行日志" <el-input
v-hasPermi="['infra:job:query']" v-model="queryParams.handlerName"
@click="handleJobLog()" placeholder="请输入处理器的名字"
clearable
@keyup.enter="handleQuery"
/> />
</template> </el-form-item>
<template #actionbtns_default="{ row }"> <el-form-item>
<!-- 操作:修改 --> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<XTextButton <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
preIcon="ep:edit" <el-button
:title="t('action.edit')" type="primary"
plain
@click="openModal('create')"
v-hasPermi="['infra:job:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:job:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="info" plain @click="handleJobLog" v-hasPermi="['infra:job:query']">
<Icon icon="ep:zoom-in" 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="name" />
<el-table-column label="任务状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="scope.row.status" />
</template> </el-table-column
>>
<el-table-column label="处理器的名字" align="center" prop="handlerName" />
<el-table-column label="处理器的参数" align="center" prop="handlerParam" />
<el-table-column label="CRON 表达式" align="center" prop="cronExpression" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
icon="el-icon-edit"
@click="openModal('update', scope.row.id)"
v-hasPermi="['infra:job:update']" v-hasPermi="['infra:job:update']"
@click="handleUpdate(row.id)" >修改</el-button
/> >
<XTextButton <el-button
preIcon="ep:edit" link
:title="row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停'" icon="el-icon-check"
@click="handleChangeStatus(scope.row)"
v-hasPermi="['infra:job:update']" v-hasPermi="['infra:job:update']"
@click="handleChangeStatus(row)" >{{ scope.row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停' }}</el-button
/> >
<!-- 操作:删除 --> <el-button
<XTextButton link
preIcon="ep:delete" icon="el-icon-delete"
:title="t('action.del')" @click="handleDelete(scope.row)"
v-hasPermi="['infra:job:delete']" v-hasPermi="['infra:job:delete']"
@click="deleteData(row.id)" >删除</el-button
/> >
<el-dropdown class="p-0.5" v-hasPermi="['infra:job:trigger', 'infra:job:query']"> <el-dropdown
<XTextButton :title="t('action.more')" postIcon="ep:arrow-down" /> class="mt-1"
:teleported="true"
@command="(command) => handleCommand(command, scope.row)"
v-hasPermi="['infra:job:trigger', 'infra:job:query']"
>
<el-button link icon="el-icon-d-arrow-right">更多</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item> <el-dropdown-item command="handleRun" v-if="hasPermi(['infra:job:trigger'])">
<!-- 操作:执行 --> 执行一次
<XTextButton
preIcon="ep:view"
title="执行一次"
v-hasPermi="['infra:job:trigger']"
@click="handleRun(row)"
/>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item> <el-dropdown-item command="handleView" v-if="hasPermi(['infra:job:query'])">
<!-- 操作:详情 --> 任务详细
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['infra:job:query']"
@click="handleDetail(row.id)"
/>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item> <el-dropdown-item command="handleJobLog" v-if="hasPermi(['infra:job:query'])">
<!-- 操作:日志 --> 调度日志
<XTextButton
preIcon="ep:view"
title="调度日志"
v-hasPermi="['infra:job:query']"
@click="handleJobLog(row.id)"
/>
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</template> </template>
</XTable> </el-table-column>
</ContentWrap> </el-table>
<XModal v-model="dialogVisible" :title="dialogTitle"> <!-- 分页组件 -->
<!-- 对话框(添加 / 修改) --> <pagination
<Form v-show="total > 0"
v-if="['create', 'update'].includes(actionType)" :total="total"
:schema="allSchemas.formSchema" v-model:page="queryParams.pageNo"
:rules="rules" v-model:limit="queryParams.pageSize"
ref="formRef" @pagination="getList"
>
<template #cronExpression="form">
<Crontab v-model="form['cronExpression']" :shortcuts="shortcuts" />
</template>
</Form>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
>
<template #retryInterval="{ row }">
<span>{{ row.retryInterval + '毫秒' }} </span>
</template>
<template #monitorTimeout="{ row }">
<span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span>
</template>
<template #nextTimes>
<span>{{ Array.from(nextTimes, (x) => parseTime(x)).join('; ') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮:保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/> />
<!-- 按钮:关闭 --> </content-wrap>
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template> <!-- 表单弹窗:添加/修改 -->
</XModal> <job-form ref="modalRef" @success="getList" />
<!-- 表单弹窗:查看 -->
<job-view ref="viewModalRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="Job"> <script setup lang="ts" name="Job">
import type { FormExpose } from '@/components/Form' import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import JobForm from './form.vue'
import JobView from './view.vue'
import download from '@/utils/download'
import * as JobApi from '@/api/infra/job' import * as JobApi from '@/api/infra/job'
import { rules, allSchemas } from './job.data'
import { InfraJobStatusEnum } from '@/utils/constants' import { InfraJobStatusEnum } from '@/utils/constants'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { push } = useRouter() const { push } = useRouter()
// 列表相关的变量 const loading = ref(true) // 列表的加载中
const [registerTable, { reload, deleteData, exportList }] = useXTable({ const total = ref(0) // 列表的总页数
allSchemas: allSchemas, const list = ref([]) // 列表的数据
getListApi: JobApi.getJobPageApi, const queryParams = reactive({
deleteApi: JobApi.deleteJobApi, pageNo: 1,
exportListApi: JobApi.exportJobApi pageSize: 10,
name: undefined,
status: undefined,
handlerName: undefined
}) })
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
// ========== CRUD 相关 ========== /** 查询参数列表 */
const actionLoading = ref(false) // 遮罩层 const getList = async () => {
const actionType = ref('') // 操作按钮的类型 loading.value = true
const dialogVisible = ref(false) // 是否显示弹出层 try {
const dialogTitle = ref('edit') // 弹出层标题 const data = await JobApi.getJobPageApi(queryParams)
const formRef = ref<FormExpose>() // 表单 Ref list.value = data.list
const detailData = ref() // 详情 Ref total.value = data.total
const nextTimes = ref([]) } finally {
const shortcuts = ref([ loading.value = false
{
text: '每天8点和12点 (自定义追加)',
value: '0 0 8,12 * * ?'
}
])
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await JobApi.getJobApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
// 设置数据
const res = await JobApi.getJobApi(rowId)
detailData.value = res
// 后续执行时长
const jobNextTime = await JobApi.getJobNextTimesApi(rowId)
nextTimes.value = jobNextTime
setDialogTile('detail')
}
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
} }
const handleChangeStatus = async (row: JobApi.JobVO) => { const handleChangeStatus = async (row: JobApi.JobVO) => {
const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭' const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
const status = const status =
row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP
message message
...@@ -249,7 +181,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => { ...@@ -249,7 +181,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
: InfraJobStatusEnum.STOP : InfraJobStatusEnum.STOP
await JobApi.updateJobStatusApi(row.id, status) await JobApi.updateJobStatusApi(row.id, status)
message.success(text + '成功') message.success(text + '成功')
await reload() await getList()
}) })
.catch(() => { .catch(() => {
row.status = row.status =
...@@ -258,6 +190,43 @@ const handleChangeStatus = async (row: JobApi.JobVO) => { ...@@ -258,6 +190,43 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
: InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.NORMAL
}) })
} }
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const modalRef = ref()
const openModal = (type: string, id?: number) => {
modalRef.value.openModal(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await JobApi.deleteJobApi(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 查看操作 */
const viewModalRef = ref()
const handleView = (rowId?: number) => {
viewModalRef.value.openModal(rowId)
}
// 执行日志 // 执行日志
const handleJobLog = (rowId?: number) => { const handleJobLog = (rowId?: number) => {
if (rowId) { if (rowId) {
...@@ -271,32 +240,61 @@ const handleRun = (row: JobApi.JobVO) => { ...@@ -271,32 +240,61 @@ const handleRun = (row: JobApi.JobVO) => {
message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder')).then(async () => { message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder')).then(async () => {
await JobApi.runJobApi(row.id) await JobApi.runJobApi(row.id)
message.success('执行成功') message.success('执行成功')
await reload() await getList()
}) })
} }
// 提交按钮
const submitForm = async () => { /** '更多'操作按钮 */
const elForm = unref(formRef)?.getElFormRef() const handleCommand = (command, row) => {
if (!elForm) return switch (command) {
elForm.validate(async (valid) => { case 'handleRun':
if (valid) { handleRun(row)
actionLoading.value = true break
// 提交请求 case 'handleView':
try { handleView(row?.id)
const data = unref(formRef)?.formModel as JobApi.JobVO break
if (actionType.value === 'create') { case 'handleJobLog':
await JobApi.createJobApi(data) handleJobLog(row?.id)
message.success(t('common.createSuccess')) break
} else { default:
await JobApi.updateJobApi(data) break
message.success(t('common.updateSuccess'))
} }
dialogVisible.value = false }
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await JobApi.exportJobApi(queryParams)
download.excel(data, '定时任务.xls')
} catch {
} finally { } finally {
actionLoading.value = false exportLoading.value = false
await reload()
}
} }
}
// 权限判断:dropdown 与 v-hasPermi有冲突会造成大量的waring,改用v-if调用此方法
const hasPermi = (permiKeys: string[]) => {
const { wsCache } = useCache()
const all_permission = '*:*:*'
const permissions = wsCache.get(CACHE_KEY.USER).permissions
if (permiKeys && permiKeys instanceof Array && permiKeys.length > 0) {
const permissionFlag = permiKeys
const hasPermissions = permissions.some((permission: string) => {
return all_permission === permission || permissionFlag.includes(permission)
}) })
return hasPermissions
}
return false
} }
/** 初始化 **/
onMounted(() => {
getList()
})
</script> </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)
// 执行查询
const data = await AccountApi.getAccountPage(params)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
} finally {
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>
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
② 代码优化,补充注释,提升阅读性 ② 代码优化,补充注释,提升阅读性
--> -->
<template> <template>
<div class="msg-main"> <ContentWrap>
<div class="msg-div" :id="'msg-div' + nowStr"> <div class="msg-div" :id="'msg-div' + nowStr">
<!-- 加载更多 --> <!-- 加载更多 -->
<div v-loading="loading"></div> <div v-loading="loading"></div>
...@@ -26,9 +26,9 @@ ...@@ -26,9 +26,9 @@
:src="item.sendFrom === 1 ? user.avatar : mp.avatar" :src="item.sendFrom === 1 ? user.avatar : mp.avatar"
class="avue-comment__avatar" class="avue-comment__avatar"
/> />
<div class="avue-comment__author">{{ <div class="avue-comment__author"
item.sendFrom === 1 ? user.nickname : mp.nickname >{{ item.sendFrom === 1 ? user.nickname : mp.nickname }}
}}</div> </div>
</div> </div>
<div class="avue-comment__main"> <div class="avue-comment__main">
<div class="avue-comment__header"> <div class="avue-comment__header">
...@@ -40,37 +40,41 @@ ...@@ -40,37 +40,41 @@
> >
<!-- 【事件】区域 --> <!-- 【事件】区域 -->
<div v-if="item.type === 'event' && item.event === 'subscribe'"> <div v-if="item.type === 'event' && item.event === 'subscribe'">
<el-tag type="success" size="mini">关注</el-tag> <el-tag type="success">关注</el-tag>
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'unsubscribe'"> <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'">
<el-tag type="danger" size="mini">取消关注</el-tag> <el-tag type="danger">取消关注</el-tag>
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'CLICK'"> <div v-else-if="item.type === 'event' && item.event === 'CLICK'">
<el-tag size="mini">点击菜单</el-tag>{{ item.eventKey }} <el-tag>点击菜单</el-tag>
{{ item.eventKey }}
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'VIEW'"> <div v-else-if="item.type === 'event' && item.event === 'VIEW'">
<el-tag size="mini">点击菜单链接</el-tag>{{ item.eventKey }} <el-tag>点击菜单链接</el-tag>
{{ item.eventKey }}
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'"> <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'">
<el-tag size="mini">扫码结果</el-tag>{{ item.eventKey }} <el-tag>扫码结果</el-tag>
{{ item.eventKey }}
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'scancode_push'"> <div v-else-if="item.type === 'event' && item.event === 'scancode_push'">
<el-tag size="mini">扫码结果</el-tag>{{ item.eventKey }} <el-tag>扫码结果</el-tag>
{{ item.eventKey }}
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'"> <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'">
<el-tag size="mini">系统拍照发图</el-tag> <el-tag>系统拍照发图</el-tag>
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'"> <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'">
<el-tag size="mini">拍照或者相册</el-tag> <el-tag>拍照或者相册</el-tag>
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'pic_weixin'"> <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'">
<el-tag size="mini">微信相册</el-tag> <el-tag>微信相册</el-tag>
</div> </div>
<div v-else-if="item.type === 'event' && item.event === 'location_select'"> <div v-else-if="item.type === 'event' && item.event === 'location_select'">
<el-tag size="mini">选择地理位置</el-tag> <el-tag>选择地理位置</el-tag>
</div> </div>
<div v-else-if="item.type === 'event'"> <div v-else-if="item.type === 'event'">
<el-tag type="danger" size="mini">未知事件类型</el-tag> <el-tag type="danger">未知事件类型</el-tag>
</div> </div>
<!-- 【消息】区域 --> <!-- 【消息】区域 -->
<div v-else-if="item.type === 'text'">{{ item.content }}</div> <div v-else-if="item.type === 'text'">{{ item.content }}</div>
...@@ -124,10 +128,10 @@ ...@@ -124,10 +128,10 @@
<wx-reply-select ref="replySelect" :objData="objData" /> <wx-reply-select ref="replySelect" :objData="objData" />
<el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button> <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
</div> </div>
</div> </ContentWrap>
</template> </template>
<script> <script lang="ts" name="WxMsg">
import { getMessagePage, sendMessage } from '@/api/mp/message' import { getMessagePage, sendMessage } from '@/api/mp/message'
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
...@@ -136,9 +140,14 @@ import WxNews from '@/views/mp/components/wx-news/main.vue' ...@@ -136,9 +140,14 @@ import WxNews from '@/views/mp/components/wx-news/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 { getUser } from '@/api/mp/mpuser' import { getUser } from '@/api/mp/mpuser'
import { defineComponent } from 'vue'
export default { const message = useMessage() // 消息弹窗
name: 'WxMsg', import profile from '@/assets/imgs/profile.jpg'
import wechat from '@/assets/imgs/wechat.png'
import { parseTime } from '@/utils/formatTime'
export default defineComponent({
components: { components: {
WxReplySelect, WxReplySelect,
WxVideoPlayer, WxVideoPlayer,
...@@ -153,160 +162,144 @@ export default { ...@@ -153,160 +162,144 @@ export default {
required: true required: true
} }
}, },
data() { setup(props) {
return { const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
nowStr: new Date().getTime(), // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 const loading = ref(false) // 消息列表是否正在加载中
loading: false, // 消息列表是否正在加载中 const loadMore = ref(true) // 是否可以加载更多
loadMore: true, // 是否可以加载更多 const list = ref<any[]>([]) // 消息列表
list: [], // 消息列表 const queryParams = reactive({
queryParams: {
pageNo: 1, // 当前页数 pageNo: 1, // 当前页数
pageSize: 14, // 每页显示多少条 pageSize: 14, // 每页显示多少条
accountId: undefined accountId: undefined
}, })
user: { const user = reactive({
// 由于微信不再提供昵称,直接使用“用户”展示 // 由于微信不再提供昵称,直接使用“用户”展示
nickname: '用户', nickname: '用户',
avatar: require('@/assets/images/profile.jpg'), avatar: profile,
accountId: 0 // 公众号账号编号 accountId: 0 // 公众号账号编号
}, })
mp: { const mp = reactive({
nickname: '公众号', nickname: '公众号',
avatar: require('@/assets/images/wechat.png') avatar: wechat
}, })
// ========= 消息发送 ========= // ========= 消息发送 =========
sendLoading: false, // 发送消息是否加载中 const sendLoading = ref(false) // 发送消息是否加载中
objData: { const objData = reactive({
// 微信发送消息 // 微信发送消息
type: 'text' type: 'text',
} accountId: null,
} articles: []
},
created() {
// 获得用户信息
getUser(this.userId).then((response) => {
this.user.nickname =
response.data.nickname && response.data.nickname.length > 0
? response.data.nickname
: this.user.nickname
this.user.avatar =
response.data.avatar && this.user.avatar.length > 0
? response.data.avatar
: this.user.avatar
this.user.accountId = response.data.accountId
// 设置公众号账号编号
this.queryParams.accountId = response.data.accountId
this.objData.accountId = response.data.accountId
// 加载消息
console.log(this.queryParams)
this.refreshChange()
}) })
},
methods: { const replySelect = ref(null)
sendMsg() { // 执行发送
if (!this.objData) { const sendMsg = async () => {
if (!objData) {
return return
} }
// 公众号限制:客服消息,公众号只允许发送一条 // // 公众号限制:客服消息,公众号只允许发送一条
if (this.objData.type === 'news' && this.objData.articles.length > 1) { if (objData.type === 'news' && objData.articles.length > 1) {
this.objData.articles = [this.objData.articles[0]] objData.articles = [objData.articles[0]]
this.$message({ message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
showClose: true,
message: '图文消息条数限制在 1 条以内,已默认发送第一条',
type: 'success'
})
} }
let data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
// 执行发送 sendLoading.value = false
this.sendLoading = true list.value = [...list.value, ...[data]]
sendMessage( scrollToBottom()
Object.assign( //ts檢查的時候會判斷這個組件可能是空的,所以需要進行斷言。
{ //避免 tab 的数据未清理
userId: this.userId const deleteObj = (replySelect.value as any).deleteObj
}, if (deleteObj) {
{ deleteObj()
...this.objData
} }
) }
) const loadingMore = () => {
.then((response) => { queryParams.pageNo++
this.sendLoading = false getPage(queryParams, null)
// 添加到消息列表,并滚动 }
this.list = [...this.list, ...[response.data]] const getPage = async (page, params) => {
this.scrollToBottom() loading.value = true
// 重置 objData 状态 let dataTemp = await getMessagePage(
this.$refs['replySelect'].deleteObj() // 重置,避免 tab 的数据未清理
})
.catch(() => {
this.sendLoading = false
})
},
loadingMore() {
this.queryParams.pageNo++
this.getPage(this.queryParams)
},
getPage(page, params) {
this.loading = true
getMessagePage(
Object.assign( Object.assign(
{ {
pageNo: page.pageNo, pageNo: page.pageNo,
pageSize: page.pageSize, pageSize: page.pageSize,
userId: this.userId, userId: props.userId,
accountId: page.accountId accountId: page.accountId
}, },
params params
) )
).then((response) => { )
// 计算当前的滚动高度 const msgDiv = document.getElementById('msg-div' + nowStr.value)
const msgDiv = document.getElementById('msg-div' + this.nowStr)
let scrollHeight = 0 let scrollHeight = 0
if (msgDiv) { if (msgDiv) {
scrollHeight = msgDiv.scrollHeight scrollHeight = msgDiv.scrollHeight
} }
// 处理数据 // 处理数据
const data = response.data.list.reverse() let data = dataTemp.list.reverse()
this.list = [...data, ...this.list] list.value = [...data, ...list.value]
this.loading = false loading.value = false
if (data.length < this.queryParams.pageSize || data.length === 0) { if (data.length < queryParams.pageSize || data.length === 0) {
this.loadMore = false loadMore.value = false
} }
this.queryParams.pageNo = page.pageNo queryParams.pageNo = page.pageNo
this.queryParams.pageSize = page.pageSize queryParams.pageSize = page.pageSize
// 滚动到原来的位置 // 滚动到原来的位置
if (this.queryParams.pageNo === 1) { if (queryParams.pageNo === 1) {
// 定位到消息底部 // 定位到消息底部
this.scrollToBottom() scrollToBottom()
} else if (data.length !== 0) { } else if (data.length !== 0) {
// 定位滚动条 // 定位滚动条
this.$nextTick(() => { await nextTick(() => {
if (scrollHeight !== 0) { if (scrollHeight !== 0) {
msgDiv.scrollTop = let div = document.getElementById('msg-div' + nowStr.value)
document.getElementById('msg-div' + this.nowStr).scrollHeight - scrollHeight - 100 if (div && msgDiv) {
msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
} }
})
} }
}) })
}, }
/** }
* 刷新回调 const refreshChange = () => {
*/ getPage(queryParams, null)
refreshChange() { }
this.getPage(this.queryParams)
},
/** 定位到消息底部 */ /** 定位到消息底部 */
scrollToBottom: function () { const scrollToBottom = () => {
this.$nextTick(() => { nextTick(() => {
let div = document.getElementById('msg-div' + this.nowStr) let div = document.getElementById('msg-div' + nowStr.value)
if (div) {
div.scrollTop = div.scrollHeight div.scrollTop = div.scrollHeight
}
})
}
onMounted(async () => {
let data = await getUser(props.userId)
user.nickname = data.nickname && data.nickname.length > 0 ? data.nickname : user.nickname
user.avatar = data.avatar && user.avatar.length > 0 ? data.avatar : user.avatar
user.accountId = data.accountId
queryParams.accountId = data.accountId
objData.accountId = data.accountId
refreshChange()
}) })
return {
sendMsg,
loadingMore,
parseTime,
scrollToBottom,
objData,
mp,
user,
queryParams,
list,
loadMore,
loading,
nowStr,
sendLoading
} }
} }
} })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */ /* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */
...@@ -317,6 +310,7 @@ export default { ...@@ -317,6 +310,7 @@ export default {
margin-top: -30px; margin-top: -30px;
padding: 10px; padding: 10px;
} }
.msg-div { .msg-div {
height: 50vh; height: 50vh;
overflow: auto; overflow: auto;
...@@ -324,13 +318,16 @@ export default { ...@@ -324,13 +318,16 @@ export default {
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;
} }
.msg-send { .msg-send {
padding: 10px; padding: 10px;
} }
.avatar-div { .avatar-div {
text-align: center; text-align: center;
width: 80px; width: 80px;
} }
.send-but { .send-but {
float: right; float: right;
margin-top: 8px !important; margin-top: 8px !important;
......
<!--&lt;!&ndash;--> <!--
<!-- - Copyright (C) 2018-2019--> - Copyright (C) 2018-2019
<!-- - All rights reserved, Designed By www.joolun.com--> - All rights reserved, Designed By www.joolun.com
<!-- 芋道源码:--> 芋道源码:
<!-- ① 移除多余的 rep 为前缀的变量,让 message 消息更简单--> ① 移除多余的 rep 为前缀的变量,让 message 消息更简单
<!-- ② 代码优化,补充注释,提升阅读性--> ② 代码优化,补充注释,提升阅读性
<!-- ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入--> ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入
<!-- ④ 支持发送【视频】消息时,支持新建视频--> ④ 支持发送【视频】消息时,支持新建视频
<!--&ndash;&gt;--> -->
<!--<template>--> <template>
<!-- <el-tabs type="border-card" v-model="objData.type" @tab-click="handleClick">--> <el-tabs type="border-card" v-model="objDataRef.type" @tab-click="handleClick">
<!-- &lt;!&ndash; 类型 1:文本 &ndash;&gt;--> <!-- 类型 1:文本 -->
<!-- <el-tab-pane name="text">--> <el-tab-pane name="text">
<!-- <span slot="label"><i class="el-icon-document"></i> 文本</span>--> <template #label>
<!-- <el-input--> <el-row align="middle">
<!-- type="textarea"--> <icon icon="ep:document" />
<!-- :rows="5"--> 文本
<!-- placeholder="请输入内容"--> </el-row>
<!-- v-model="objData.content"--> </template>
<!-- @input="inputContent"--> <el-input
<!-- />--> type="textarea"
<!-- </el-tab-pane>--> :rows="5"
<!-- &lt;!&ndash; 类型 2:图片 &ndash;&gt;--> placeholder="请输入内容"
<!-- <el-tab-pane name="image">--> v-model="objDataRef.content"
<!-- <span slot="label"><i class="el-icon-picture"></i> 图片</span>--> @input="inputContent"
<!-- <el-row>--> />
<!-- &lt;!&ndash; 情况一:已经选择好素材、或者上传好图片 &ndash;&gt;--> </el-tab-pane>
<!-- <div class="select-item" v-if="objData.url">--> <!-- 类型 2:图片 -->
<!-- <img class="material-img" :src="objData.url" />--> <el-tab-pane name="image">
<!-- <p class="item-name" v-if="objData.name">{{ objData.name }}</p>--> <template #label>
<!-- <el-row class="ope-row">--> <el-row align="middle">
<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />--> <icon icon="ep:picture" class="mr-5px" />
<!-- </el-row>--> 图片
<!-- </div>--> </el-row>
<!-- &lt;!&ndash; 情况二:未做完上述操作 &ndash;&gt;--> </template>
<!-- <div v-else>--> <!-- 情况一:已经选择好素材、或者上传好图片 -->
<!-- <el-row style="text-align: center">--> <div class="select-item" v-if="objDataRef.url">
<!-- &lt;!&ndash; 选择素材 &ndash;&gt;--> <img class="material-img" :src="objDataRef.url" />
<!-- <el-col :span="12" class="col-select">--> <p class="item-name" v-if="objDataRef.name">{{ objDataRef.name }}</p>
<!-- <el-button type="success" @click="openMaterial">--> <el-row class="ope-row">
<!-- 素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;right"></i>--> <el-button type="danger" circle @click="deleteObj">
<!-- </el-button>--> <icon icon="ep:delete" />
<!-- <el-dialog--> </el-button>
<!-- title="选择图片"--> </el-row>
<!-- v-model:visible="dialogImageVisible"--> </div>
<!-- width="90%"--> <!-- 情况二:未做完上述操作 -->
<!-- append-to-body--> <el-row v-else style="text-align: center" align="middle">
<!-- >--> <!-- 选择素材 -->
<!-- <wx-material-select :obj-data="objData" @selectMaterial="selectMaterial" />--> <el-col :span="12" class="col-select">
<!-- </el-dialog>--> <el-button type="success" @click="openMaterial">
<!-- </el-col>--> 素材库选择
<!-- &lt;!&ndash; 文件上传 &ndash;&gt;--> <icon icon="ep:circle-check" />
<!-- <el-col :span="12" class="col-add">--> </el-button>
<!-- <el-upload--> <el-dialog title="选择图片" v-model="dialogImageVisible" width="90%" append-to-body>
<!-- :action="actionUrl"--> <wx-material-select :obj-data="objDataRef" @selectMaterial="selectMaterial" />
<!-- :headers="headers"--> </el-dialog>
<!-- multiple--> </el-col>
<!-- :limit="1"--> <!-- 文件上传 -->
<!-- :file-list="fileList"--> <el-col :span="12" class="col-add">
<!-- :data="uploadData"--> <el-upload
<!-- :before-upload="beforeImageUpload"--> :action="actionUrl"
<!-- :on-success="handleUploadSuccess"--> :headers="headers"
<!-- >--> multiple
<!-- <el-button type="primary">上传图片</el-button>--> :limit="1"
<!-- <div slot="tip" class="el-upload__tip"--> :file-list="fileList"
<!-- >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div--> :data="uploadData"
<!-- >--> :before-upload="beforeImageUpload"
<!-- </el-upload>--> :on-success="handleUploadSuccess"
<!-- </el-col>--> >
<!-- </el-row>--> <el-button type="primary">上传图片</el-button>
<!-- </div>--> <template #tip>
<!-- </el-row>--> <span>
<!-- </el-tab-pane>--> <div class="el-upload__tip"
<!-- &lt;!&ndash; 类型 3:语音 &ndash;&gt;--> >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div
<!-- <el-tab-pane name="voice">--> ></span
<!-- <span slot="label"><i class="el-icon-phone"></i> 语音</span>--> >
<!-- <el-row>--> </template>
<!-- <div class="select-item2" v-if="objData.url">--> </el-upload>
<!-- <p class="item-name">{{ objData.name }}</p>--> </el-col>
<!-- <div class="item-infos">--> </el-row>
<!-- <wx-voice-player :url="objData.url" />--> </el-tab-pane>
<!-- </div>--> <!-- 类型 3:语音 -->
<!-- <el-row class="ope-row">--> <el-tab-pane name="voice">
<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />--> <template #label>
<!-- </el-row>--> <el-row align="middle">
<!-- </div>--> <icon icon="ep:phone" />
<!-- <div v-else>--> 语音
<!-- <el-row style="text-align: center">--> </el-row>
<!-- &lt;!&ndash; 选择素材 &ndash;&gt;--> </template>
<!-- <el-col :span="12" class="col-select">-->
<!-- <el-button type="success" @click="openMaterial">-->
<!-- 素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;right"></i>-->
<!-- </el-button>-->
<!-- <el-dialog-->
<!-- title="选择语音"-->
<!-- v-model:visible="dialogVoiceVisible"-->
<!-- width="90%"-->
<!-- append-to-body-->
<!-- >-->
<!-- <WxMaterialSelect :objData="objData" @selectMaterial="selectMaterial" />-->
<!-- </el-dialog>-->
<!-- </el-col>-->
<!-- &lt;!&ndash; 文件上传 &ndash;&gt;-->
<!-- <el-col :span="12" class="col-add">-->
<!-- <el-upload-->
<!-- :action="actionUrl"-->
<!-- :headers="headers"-->
<!-- multiple-->
<!-- :limit="1"-->
<!-- :file-list="fileList"-->
<!-- :data="uploadData"-->
<!-- :before-upload="beforeVoiceUpload"-->
<!-- :on-success="handleUploadSuccess"-->
<!-- >-->
<!-- <el-button type="primary">点击上传</el-button>-->
<!-- <div slot="tip" class="el-upload__tip"-->
<!-- >格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s</div-->
<!-- >-->
<!-- </el-upload>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </div>-->
<!-- </el-row>-->
<!-- </el-tab-pane>-->
<!-- &lt;!&ndash; 类型 4:视频 &ndash;&gt;-->
<!-- <el-tab-pane name="video">-->
<!-- <span slot="label"><i class="el-icon-share"></i> 视频</span>-->
<!-- <el-row>-->
<!-- <el-input v-model="objData.title" placeholder="请输入标题" @input="inputContent" />-->
<!-- <div style="margin: 20px 0"></div>-->
<!-- <el-input v-model="objData.description" placeholder="请输入描述" @input="inputContent" />-->
<!-- <div style="margin: 20px 0"></div>-->
<!-- <div style="text-align: center">-->
<!-- <wx-video-player v-if="objData.url" :url="objData.url" />-->
<!-- </div>-->
<!-- <div style="margin: 20px 0"></div>-->
<!-- <el-row style="text-align: center">-->
<!-- &lt;!&ndash; 选择素材 &ndash;&gt;-->
<!-- <el-col :span="12">-->
<!-- <el-button type="success" @click="openMaterial">-->
<!-- 素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;right"></i>-->
<!-- </el-button>-->
<!-- <el-dialog-->
<!-- title="选择视频"-->
<!-- v-model:visible="dialogVideoVisible"-->
<!-- width="90%"-->
<!-- append-to-body-->
<!-- >-->
<!-- <wx-material-select :objData="objData" @selectMaterial="selectMaterial" />-->
<!-- </el-dialog>-->
<!-- </el-col>-->
<!-- &lt;!&ndash; 文件上传 &ndash;&gt;-->
<!-- <el-col :span="12">-->
<!-- <el-upload-->
<!-- :action="actionUrl"-->
<!-- :headers="headers"-->
<!-- multiple-->
<!-- :limit="1"-->
<!-- :file-list="fileList"-->
<!-- :data="uploadData"-->
<!-- :before-upload="beforeVideoUpload"-->
<!-- :on-success="handleUploadSuccess"-->
<!-- >-->
<!-- <el-button type="primary"-->
<!-- >新建视频<i class="el-icon-upload el-icon&#45;&#45;right"></i-->
<!-- ></el-button>-->
<!-- </el-upload>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </el-row>-->
<!-- </el-tab-pane>-->
<!-- &lt;!&ndash; 类型 5:图文 &ndash;&gt;-->
<!-- <el-tab-pane name="news">-->
<!-- <span slot="label"><i class="el-icon-news"></i> 图文</span>-->
<!-- <el-row>-->
<!-- <div class="select-item" v-if="objData.articles">-->
<!-- <wx-news :articles="objData.articles" />-->
<!-- <el-row class="ope-row">-->
<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />-->
<!-- </el-row>-->
<!-- </div>-->
<!-- &lt;!&ndash; 选择素材 &ndash;&gt;-->
<!-- <div v-if="!objData.content">-->
<!-- <el-row style="text-align: center">-->
<!-- <el-col :span="24">-->
<!-- <el-button type="success" @click="openMaterial"-->
<!-- >{{ newsType === '1' ? '选择已发布图文' : '选择草稿箱图文'-->
<!-- }}<i class="el-icon-circle-check el-icon&#45;&#45;right"></i-->
<!-- ></el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </div>-->
<!-- <el-dialog title="选择图文" v-model:visible="dialogNewsVisible" width="90%" append-to-body>-->
<!-- <wx-material-select-->
<!-- :objData="objData"-->
<!-- @selectMaterial="selectMaterial"-->
<!-- :newsType="newsType"-->
<!-- />-->
<!-- </el-dialog>-->
<!-- </el-row>-->
<!-- </el-tab-pane>-->
<!-- &lt;!&ndash; 类型 6:音乐 &ndash;&gt;-->
<!-- <el-tab-pane name="music">-->
<!-- <span slot="label"><i class="el-icon-service"></i> 音乐</span>-->
<!-- <el-row>-->
<!-- <el-col :span="6">-->
<!-- <div class="thumb-div">-->
<!-- <img style="width: 100px" v-if="objData.thumbMediaUrl" :src="objData.thumbMediaUrl" />-->
<!-- <i v-else class="el-icon-plus avatar-uploader-icon"></i>-->
<!-- <div class="thumb-but">-->
<!-- <el-upload-->
<!-- :action="actionUrl"-->
<!-- :headers="headers"-->
<!-- multiple-->
<!-- :limit="1"-->
<!-- :file-list="fileList"-->
<!-- :data="uploadData"-->
<!-- :before-upload="beforeThumbImageUpload"-->
<!-- :on-success="handleUploadSuccess"-->
<!-- >-->
<!-- <el-button slot="trigger" size="mini" type="text">本地上传</el-button>-->
<!-- <el-button size="mini" type="text" @click="openMaterial" style="margin-left: 5px"-->
<!-- >素材库选择</el-button-->
<!-- >-->
<!-- </el-upload>-->
<!-- </div>-->
<!-- </div>-->
<!-- <el-dialog-->
<!-- title="选择图片"-->
<!-- v-model:visible="dialogThumbVisible"-->
<!-- width="80%"-->
<!-- append-to-body-->
<!-- >-->
<!-- <wx-material-select-->
<!-- :objData="{ type: 'image', accountId: objData.accountId }"-->
<!-- @selectMaterial="selectMaterial"-->
<!-- />-->
<!-- </el-dialog>-->
<!-- </el-col>-->
<!-- <el-col :span="18">-->
<!-- <el-input v-model="objData.title" placeholder="请输入标题" @input="inputContent" />-->
<!-- <div style="margin: 20px 0"></div>-->
<!-- <el-input v-model="objData.description" placeholder="请输入描述" @input="inputContent" />-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <div style="margin: 20px 0"></div>-->
<!-- <el-input v-model="objData.musicUrl" placeholder="请输入音乐链接" @input="inputContent" />-->
<!-- <div style="margin: 20px 0"></div>-->
<!-- <el-input-->
<!-- v-model="objData.hqMusicUrl"-->
<!-- placeholder="请输入高质量音乐链接"-->
<!-- @input="inputContent"-->
<!-- />-->
<!-- </el-tab-pane>-->
<!-- </el-tabs>-->
<!--</template>-->
<!--<script>--> <div class="select-item2" v-if="objDataRef.url">
<!--import WxNews from '@/views/mp/components/wx-news/main.vue'--> <p class="item-name">{{ objDataRef.name }}</p>
<!--import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'--> <div class="item-infos">
<!--import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'--> <wx-voice-player :url="objDataRef.url" />
<!--import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'--> </div>
<el-row class="ope-row">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />
</el-row>
</div>
<el-row v-else style="text-align: center">
<!-- 选择素材 -->
<el-col :span="12" class="col-select">
<el-button type="success" @click="openMaterial">
素材库选择<i class="el-icon-circle-check el-icon--right"></i>
</el-button>
<el-dialog title="选择语音" v-model="dialogVoiceVisible" width="90%" append-to-body>
<WxMaterialSelect :objData="objData" @selectMaterial="selectMaterial" />
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12" class="col-add">
<el-upload
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVoiceUpload"
:on-success="handleUploadSuccess"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip"
>格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s
</div>
</template>
</el-upload>
</el-col>
</el-row>
</el-tab-pane>
<!-- 类型 4:视频 -->
<el-tab-pane name="video">
<template #label>
<el-row align="middle">
<icon icon="ep:share" />
视频
</el-row>
</template>
<el-row>
<el-input
v-model="objDataRef.title"
class="input-margin-bottom"
placeholder="请输入标题"
@input="inputContent"
/>
<el-input
class="input-margin-bottom"
v-model="objDataRef.description"
placeholder="请输入描述"
@input="inputContent"
/>
<div style="text-align: center">
<wx-video-player v-if="objDataRef.url" :url="objDataRef.url" />
</div>
<el-col>
<el-row style="text-align: center" align="middle">
<!-- 选择素材 -->
<el-col :span="12">
<el-button type="success" @click="openMaterial">
素材库选择
<icon icon="ep:circle-check" />
</el-button>
<el-dialog title="选择视频" v-model="dialogVideoVisible" width="90%" append-to-body>
<wx-material-select :objData="objDataRef" @selectMaterial="selectMaterial" />
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12">
<el-upload
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVideoUpload"
:on-success="handleUploadSuccess"
>
<el-button type="primary"
>新建视频
<icon icon="ep:upload" />
</el-button>
</el-upload>
</el-col>
</el-row>
</el-col>
</el-row>
</el-tab-pane>
<!-- 类型 5:图文 -->
<el-tab-pane name="news">
<template #label>
<el-row align="middle">
<icon icon="ep:reading" />
图文
</el-row>
</template>
<el-row>
<div class="select-item" v-if="objDataRef.articles.size > 0">
<wx-news :articles="objDataRef.articles" />
<el-col class="ope-row">
<el-button type="danger" circle @click="deleteObj">
<icon icon="ep:delete" />
</el-button>
</el-col>
</div>
<!-- 选择素材 -->
<el-col :span="24" v-if="!objDataRef.content">
<el-row style="text-align: center" align="middle">
<el-col :span="24">
<el-button type="success" @click="openMaterial"
>{{ newsType === '1' ? '选择已发布图文' : '选择草稿箱图文' }}
<icon icon="ep:circle-check" />
</el-button>
</el-col>
</el-row>
</el-col>
<el-dialog title="选择图文" v-model="dialogNewsVisible" width="90%" append-to-body>
<wx-material-select
:objData="objDataRef"
@selectMaterial="selectMaterial"
:newsType="newsType"
/>
</el-dialog>
</el-row>
</el-tab-pane>
<!-- 类型 6:音乐 -->
<el-tab-pane name="music">
<template #label>
<el-row align="middle">
<icon icon="ep:service" />
音乐
</el-row>
</template>
<el-row align="middle" justify="center">
<el-col :span="6">
<el-row align="middle" justify="center" class="thumb-div">
<el-col :span="24">
<el-row align="middle" justify="center">
<img
style="width: 100px"
v-if="objDataRef.thumbMediaUrl"
:src="objDataRef.thumbMediaUrl"
/>
<icon v-else icon="ep:plus" />
</el-row>
<el-row align="middle" justify="center" style="margin-top: 2%">
<div class="thumb-but">
<el-upload
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeThumbImageUpload"
:on-success="handleUploadSuccess"
>
<template #trigger>
<el-button type="text">本地上传</el-button>
</template>
<el-button type="text" @click="openMaterial" style="margin-left: 5px"
>素材库选择
</el-button>
</el-upload>
</div>
</el-row>
</el-col>
</el-row>
<el-dialog title="选择图片" v-model="dialogThumbVisible" width="80%" append-to-body>
<wx-material-select
:objData="{ type: 'image', accountId: objDataRef.accountId }"
@selectMaterial="selectMaterial"
/>
</el-dialog>
</el-col>
<el-col :span="18">
<el-input v-model="objDataRef.title" placeholder="请输入标题" @input="inputContent" />
<div style="margin: 20px 0"></div>
<el-input
v-model="objDataRef.description"
placeholder="请输入描述"
@input="inputContent"
/>
</el-col>
</el-row>
<div style="margin: 20px 0"></div>
<el-input v-model="objDataRef.musicUrl" placeholder="请输入音乐链接" @input="inputContent" />
<div style="margin: 20px 0"></div>
<el-input
v-model="objDataRef.hqMusicUrl"
placeholder="请输入高质量音乐链接"
@input="inputContent"
/>
</el-tab-pane>
</el-tabs>
</template>
<!--import { getAccessToken } from '@/utils/auth'--> <script lang="ts" name="WxReplySelect">
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
<!--export default {--> import { getAccessToken } from '@/utils/auth'
<!-- name: 'WxReplySelect',--> import { defineComponent } from 'vue'
<!-- components: {-->
<!-- WxNews,-->
<!-- WxMaterialSelect,-->
<!-- WxVoicePlayer,-->
<!-- WxVideoPlayer-->
<!-- },-->
<!-- props: {-->
<!-- objData: {-->
<!-- // 消息对象。-->
<!-- type: Object, // 设置为 Object 的原因,方便属性的传递-->
<!-- required: true-->
<!-- },-->
<!-- newsType: {-->
<!-- // 图文类型:1、已发布图文;2、草稿箱图文-->
<!-- type: String,-->
<!-- default: '1'-->
<!-- }-->
<!-- },-->
<!-- data() {-->
<!-- return {-->
<!-- tempPlayerObj: {-->
<!-- type: '2'-->
<!-- },-->
<!-- tempObj: new Map().set(--> export default defineComponent({
<!-- // 临时缓存,切换消息类型的 tab 的时候,可以保存对应的数据;--> components: {
<!-- this.objData.type, // 消息类型--> WxNews,
<!-- Object.assign({}, this.objData)--> WxMaterialSelect,
<!-- ), // 消息内容--> WxVoicePlayer,
WxVideoPlayer
},
props: {
objData: {
// 消息对象。
type: Object, // 设置为 Object 的原因,方便属性的传递
required: true
},
newsType: {
// 图文类型:1、已发布图文;2、草稿箱图文
type: String,
default: '1'
}
},
setup(props) {
const objDataRef = reactive(props.objData)
const message = useMessage() // 消息弹窗
const tempObj = new Map().set(objDataRef.type, Object.assign({}, objDataRef))
// ========== 素材选择的弹窗,是否可见 ==========
const dialogNewsVisible = ref(false) // 图文
const dialogImageVisible = ref(false) // 图片
const dialogVoiceVisible = ref(false) // 语音
const dialogVideoVisible = ref(false) // 视频
const dialogThumbVisible = ref(false) // 缩略图
// ========== 文件上传(图片、语音、视频) ==========
const fileList = ref([])
const uploadData = reactive({
accountId: undefined,
type: objDataRef.type,
title: '',
introduction: ''
})
const actionUrl = ref(
import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
)
const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部
const beforeThumbImageUpload = (file) => {
const isType =
file.type === 'image/jpeg' ||
file.type === 'image/png' ||
file.type === 'image/gif' ||
file.type === 'image/bmp' ||
file.type === 'image/jpg'
if (!isType) {
message.error('上传图片格式不对!')
return false
}
const isLt = file.size / 1024 / 1024 < 2
if (!isLt) {
message.error('上传图片大小不能超过 2M!')
return false
}
uploadData.accountId = objDataRef.accountId
return true
}
const beforeVoiceUpload = (file) => {
// 校验格式
const isType =
file.type === 'audio/mp3' ||
file.type === 'audio/mpeg' ||
file.type === 'audio/wma' ||
file.type === 'audio/wav' ||
file.type === 'audio/amr'
if (!isType) {
message.error('上传语音格式不对!' + file.type)
return false
}
// 校验大小
const isLt = file.size / 1024 / 1024 < 2
if (!isLt) {
message.error('上传语音大小不能超过 2M!')
return false
}
uploadData.accountId = objDataRef.accountId
return true
}
const beforeImageUpload = (file) => {
// 校验格式
const isType =
file.type === 'image/jpeg' ||
file.type === 'image/png' ||
file.type === 'image/gif' ||
file.type === 'image/bmp' ||
file.type === 'image/jpg'
if (!isType) {
message.error('上传图片格式不对!')
return false
}
// 校验大小
const isLt = file.size / 1024 / 1024 < 2
if (!isLt) {
message.error('上传图片大小不能超过 2M!')
return false
}
uploadData.accountId = objDataRef.accountId
return true
}
const beforeVideoUpload = (file) => {
// 校验格式
const isType = file.type === 'video/mp4'
if (!isType) {
message.error('上传视频格式不对!')
return false
}
// 校验大小
const isLt = file.size / 1024 / 1024 < 10
if (!isLt) {
message.error('上传视频大小不能超过 10M!')
return false
}
uploadData.accountId = objDataRef.accountId
return true
}
const handleUploadSuccess = (response) => {
if (response.code !== 0) {
message.error('上传出错:' + response.msg)
return false
}
<!-- // ========== 素材选择的弹窗,是否可见 ==========--> // 清空上传时的各种数据
<!-- dialogNewsVisible: false, // 图文--> fileList.value = []
<!-- dialogImageVisible: false, // 图片--> uploadData.title = ''
<!-- dialogVoiceVisible: false, // 语音--> uploadData.introduction = ''
<!-- dialogVideoVisible: false, // 视频-->
<!-- dialogThumbVisible: false, // 缩略图-->
<!-- // ========== 文件上传(图片、语音、视频) ==========--> // 上传好的文件,本质是个素材,所以可以进行选中
<!-- fileList: [], // 文件列表--> let item = response.data
<!-- uploadData: {--> selectMaterial(item)
<!-- accountId: undefined,--> }
<!-- type: this.objData.type,--> /**
<!-- title: '',--> * 切换消息类型的 tab
<!-- introduction: ''--> *
<!-- },--> * @param tab tab 没用 暂时删了tab
<!-- actionUrl: process.env.VUE_APP_BASE_API + '/admin-api/mp/material/upload-temporary',--> */
<!-- headers: { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部--> const handleClick = () => {
<!-- }--> // 设置后续文件上传的文件类型
<!-- },--> uploadData.type = objDataRef.type
<!-- methods: {--> if (uploadData.type === 'music') {
<!-- beforeThumbImageUpload(file) {--> // 【音乐】上传的是缩略图
<!-- const isType =--> uploadData.type = 'thumb'
<!-- file.type === 'image/jpeg' ||--> }
<!-- file.type === 'image/png' ||-->
<!-- file.type === 'image/gif' ||-->
<!-- file.type === 'image/bmp' ||-->
<!-- file.type === 'image/jpg'-->
<!-- if (!isType) {-->
<!-- this.$message.error('上传图片格式不对!')-->
<!-- return false-->
<!-- }-->
<!-- const isLt = file.size / 1024 / 1024 < 2-->
<!-- if (!isLt) {-->
<!-- this.$message.error('上传图片大小不能超过 2M!')-->
<!-- return false-->
<!-- }-->
<!-- this.uploadData.accountId = this.objData.accountId-->
<!-- return true-->
<!-- },-->
<!-- beforeVoiceUpload(file) {-->
<!-- // 校验格式-->
<!-- const isType =-->
<!-- file.type === 'audio/mp3' ||-->
<!-- file.type === 'audio/mpeg' ||-->
<!-- file.type === 'audio/wma' ||-->
<!-- file.type === 'audio/wav' ||-->
<!-- file.type === 'audio/amr'-->
<!-- if (!isType) {-->
<!-- this.$message.error('上传语音格式不对!' + file.type)-->
<!-- return false-->
<!-- }-->
<!-- // 校验大小-->
<!-- const isLt = file.size / 1024 / 1024 < 2-->
<!-- if (!isLt) {-->
<!-- this.$message.error('上传语音大小不能超过 2M!')-->
<!-- return false-->
<!-- }-->
<!-- this.uploadData.accountId = this.objData.accountId-->
<!-- return true-->
<!-- },-->
<!-- beforeImageUpload(file) {-->
<!-- // 校验格式-->
<!-- const isType =-->
<!-- file.type === 'image/jpeg' ||-->
<!-- file.type === 'image/png' ||-->
<!-- file.type === 'image/gif' ||-->
<!-- file.type === 'image/bmp' ||-->
<!-- file.type === 'image/jpg'-->
<!-- if (!isType) {-->
<!-- this.$message.error('上传图片格式不对!')-->
<!-- return false-->
<!-- }-->
<!-- // 校验大小-->
<!-- const isLt = file.size / 1024 / 1024 < 2-->
<!-- if (!isLt) {-->
<!-- this.$message.error('上传图片大小不能超过 2M!')-->
<!-- return false-->
<!-- }-->
<!-- this.uploadData.accountId = this.objData.accountId-->
<!-- return true-->
<!-- },-->
<!-- beforeVideoUpload(file) {-->
<!-- // 校验格式-->
<!-- const isType = file.type === 'video/mp4'-->
<!-- if (!isType) {-->
<!-- this.$message.error('上传视频格式不对!')-->
<!-- return false-->
<!-- }-->
<!-- // 校验大小-->
<!-- const isLt = file.size / 1024 / 1024 < 10-->
<!-- if (!isLt) {-->
<!-- this.$message.error('上传视频大小不能超过 10M!')-->
<!-- return false-->
<!-- }-->
<!-- this.uploadData.accountId = this.objData.accountId-->
<!-- return true-->
<!-- },-->
<!-- handleUploadSuccess(response, file, fileList) {-->
<!-- if (response.code !== 0) {-->
<!-- this.$message.error('上传出错:' + response.msg)-->
<!-- return false-->
<!-- }-->
<!-- // 清空上传时的各种数据--> // 从 tempObj 临时缓存中,获取对应的数据,并设置回 objDataRef
<!-- this.fileList = []--> let tempObjItem = tempObj.get(objDataRef.type)
<!-- this.uploadData.title = ''--> if (tempObjItem) {
<!-- this.uploadData.introduction = ''--> objDataRef.content = tempObjItem.content ? tempObjItem.content : null
objDataRef.mediaId = tempObjItem.mediaId ? tempObjItem.mediaId : null
objDataRef.url = tempObjItem.url ? tempObjItem.url : null
objDataRef.name = tempObjItem.url ? tempObjItem.name : null
objDataRef.title = tempObjItem.title ? tempObjItem.title : null
objDataRef.description = tempObjItem.description ? tempObjItem.description : null
return
}
// 如果获取不到,需要把 objDataRef 复原
// 必须使用 $set 赋值,不然 input 无法输入内容
objDataRef.content = ''
objDataRef.mediaId = ''
objDataRef.url = ''
objDataRef.title = ''
objDataRef.description = ''
}
/**
* 选择素材,将设置设置到 objDataRef 变量
*
* @param item 素材
*/
const selectMaterial = (item) => {
// 选择好素材,所以隐藏弹窗
closeMaterial()
<!-- // 上传好的文件,本质是个素材,所以可以进行选中--> // 创建 tempObjItem 对象,并设置对应的值
<!-- let item = response.data--> let tempObjItem = {
<!-- this.selectMaterial(item)--> type: '',
<!-- },--> articles: '',
<!-- /**--> thumbMediaId: '',
<!-- * 切换消息类型的 tab--> thumbMediaUrl: '',
<!-- *--> introduction: '',
<!-- * @param tab tab--> title: '',
<!-- */--> musicUrl: '',
<!-- handleClick(tab) {--> hqMusicUrl: '',
<!-- // 设置后续文件上传的文件类型--> mediaId: '',
<!-- this.uploadData.type = this.objData.type--> url: '',
<!-- if (this.uploadData.type === 'music') {--> name: '',
<!-- // 【音乐】上传的是缩略图--> description: ''
<!-- this.uploadData.type = 'thumb'--> }
<!-- }--> tempObjItem.type = objDataRef.type
if (objDataRef.type === 'news') {
tempObjItem.articles = item.content.newsItem
objDataRef.articles = item.content.newsItem
} else if (objDataRef.type === 'music') {
// 音乐需要特殊处理,因为选择的是图片的缩略图
tempObjItem.thumbMediaId = item.mediaId
objDataRef.thumbMediaId = item.mediaId
tempObjItem.thumbMediaUrl = item.url
objDataRef.thumbMediaUrl = item.url
// title、introduction、musicUrl、hqMusicUrl:从 objDataRef 到 tempObjItem,避免上传素材后,被覆盖掉
tempObjItem.title = objDataRef.title || ''
tempObjItem.introduction = objDataRef.introduction || ''
tempObjItem.musicUrl = objDataRef.musicUrl || ''
tempObjItem.hqMusicUrl = objDataRef.hqMusicUrl || ''
} else if (objDataRef.type === 'image' || objDataRef.type === 'voice') {
tempObjItem.mediaId = item.mediaId
objDataRef.mediaId = item.mediaId
tempObjItem.url = item.url
objDataRef.url = item.url
tempObjItem.name = item.name
objDataRef.name = item.name
} else if (objDataRef.type === 'video') {
tempObjItem.mediaId = item.mediaId
objDataRef.mediaId = item.mediaId
tempObjItem.url = item.url
objDataRef.url = item.url
tempObjItem.name = item.name
objDataRef.name = item.name
// title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction
if (item.title) {
objDataRef.title = item.title || ''
tempObjItem.title = item.title || ''
}
if (item.introduction) {
objDataRef.description = item.introduction || '' // 消息使用的是 description,素材使用的是 introduction,所以转换下
tempObjItem.description = item.introduction || ''
}
} else if (objDataRef.type === 'text') {
objDataRef.content = item.content || ''
}
// 最终设置到临时缓存
tempObj.set(objDataRef.type, tempObjItem)
}
const openMaterial = () => {
if (objDataRef.type === 'news') {
dialogNewsVisible.value = true
} else if (objDataRef.type === 'image') {
dialogImageVisible.value = true
} else if (objDataRef.type === 'voice') {
dialogVoiceVisible.value = true
} else if (objDataRef.type === 'video') {
dialogVideoVisible.value = true
} else if (objDataRef.type === 'music') {
dialogThumbVisible.value = true
}
}
const closeMaterial = () => {
dialogNewsVisible.value = false
dialogImageVisible.value = false
dialogVoiceVisible.value = false
dialogVideoVisible.value = false
dialogThumbVisible.value = false
}
const deleteObj = () => {
if (objDataRef.type === 'news') {
objDataRef.articles = ''
} else if (objDataRef.type === 'image') {
objDataRef.mediaId = null
objDataRef.url = null
objDataRef.name = null
} else if (objDataRef.type === 'voice') {
objDataRef.mediaId = null
objDataRef.url = null
objDataRef.name = null
} else if (objDataRef.type === 'video') {
objDataRef.mediaId = null
objDataRef.url = null
objDataRef.name = null
objDataRef.title = null
objDataRef.description = null
} else if (objDataRef.type === 'music') {
objDataRef.thumbMediaId = null
objDataRef.thumbMediaUrl = null
objDataRef.title = null
objDataRef.description = null
objDataRef.musicUrl = null
objDataRef.hqMusicUrl = null
} else if (objDataRef.type === 'text') {
objDataRef.content = null
}
// 覆盖缓存
tempObj.set(objDataRef.type, Object.assign({}, objDataRef))
}
/**
* 输入时,缓存每次 objDataRef 到 tempObj 中
*
* why?不确定为什么 v-model="objDataRef.content" 不能自动缓存,所以通过这样的方式
*/
const inputContent = () => {
// 覆盖缓存
tempObj.set(objDataRef.type, Object.assign({}, objDataRef))
}
return {
inputContent,
dialogNewsVisible,
deleteObj,
openMaterial,
handleClick,
beforeImageUpload,
beforeVoiceUpload,
handleUploadSuccess,
beforeVideoUpload,
selectMaterial,
dialogImageVisible,
dialogVoiceVisible,
dialogThumbVisible,
actionUrl,
objDataRef,
headers,
fileList,
beforeThumbImageUpload,
uploadData,
dialogVideoVisible
}
}
})
</script>
<!-- // 从 tempObj 临时缓存中,获取对应的数据,并设置回 objData--> <style lang="scss" scoped>
<!-- let tempObjItem = this.tempObj.get(this.objData.type)--> .public-account-management {
<!-- if (tempObjItem) {--> .el-input {
<!-- this.objData.content = tempObjItem.content ? tempObjItem.content : null--> width: 70%;
<!-- this.objData.mediaId = tempObjItem.mediaId ? tempObjItem.mediaId : null--> margin-right: 2%;
<!-- this.objData.url = tempObjItem.url ? tempObjItem.url : null--> }
<!-- this.objData.name = tempObjItem.url ? tempObjItem.name : null--> }
<!-- this.objData.title = tempObjItem.title ? tempObjItem.title : null-->
<!-- this.objData.description = tempObjItem.description ? tempObjItem.description : null-->
<!-- return-->
<!-- }-->
<!-- // 如果获取不到,需要把 objData 复原-->
<!-- // 必须使用 $set 赋值,不然 input 无法输入内容-->
<!-- this.$set(this.objData, 'content', '')-->
<!-- this.$delete(this.objData, 'mediaId')-->
<!-- this.$delete(this.objData, 'url')-->
<!-- this.$set(this.objData, 'title', '')-->
<!-- this.$set(this.objData, 'description', '')-->
<!-- },-->
<!-- /**-->
<!-- * 选择素材,将设置设置到 objData 变量-->
<!-- *-->
<!-- * @param item 素材-->
<!-- */-->
<!-- selectMaterial(item) {-->
<!-- // 选择好素材,所以隐藏弹窗-->
<!-- this.closeMaterial()-->
<!-- // 创建 tempObjItem 对象,并设置对应的值--> .pagination {
<!-- let tempObjItem = {}--> text-align: right;
<!-- tempObjItem.type = this.objData.type--> margin-right: 25px;
<!-- if (this.objData.type === 'news') {--> }
<!-- tempObjItem.articles = item.content.newsItem-->
<!-- this.objData.articles = item.content.newsItem-->
<!-- } else if (this.objData.type === 'music') {-->
<!-- // 音乐需要特殊处理,因为选择的是图片的缩略图-->
<!-- tempObjItem.thumbMediaId = item.mediaId-->
<!-- this.objData.thumbMediaId = item.mediaId-->
<!-- tempObjItem.thumbMediaUrl = item.url-->
<!-- this.objData.thumbMediaUrl = item.url-->
<!-- // title、introduction、musicUrl、hqMusicUrl:从 objData 到 tempObjItem,避免上传素材后,被覆盖掉-->
<!-- tempObjItem.title = this.objData.title || ''-->
<!-- tempObjItem.introduction = this.objData.introduction || ''-->
<!-- tempObjItem.musicUrl = this.objData.musicUrl || ''-->
<!-- tempObjItem.hqMusicUrl = this.objData.hqMusicUrl || ''-->
<!-- } else if (this.objData.type === 'image' || this.objData.type === 'voice') {-->
<!-- tempObjItem.mediaId = item.mediaId-->
<!-- this.objData.mediaId = item.mediaId-->
<!-- tempObjItem.url = item.url-->
<!-- this.objData.url = item.url-->
<!-- tempObjItem.name = item.name-->
<!-- this.objData.name = item.name-->
<!-- } else if (this.objData.type === 'video') {-->
<!-- tempObjItem.mediaId = item.mediaId-->
<!-- this.objData.mediaId = item.mediaId-->
<!-- tempObjItem.url = item.url-->
<!-- this.objData.url = item.url-->
<!-- tempObjItem.name = item.name-->
<!-- this.objData.name = item.name-->
<!-- // title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction-->
<!-- if (item.title) {-->
<!-- this.objData.title = item.title || ''-->
<!-- tempObjItem.title = item.title || ''-->
<!-- }-->
<!-- if (item.introduction) {-->
<!-- this.objData.description = item.introduction || '' // 消息使用的是 description,素材使用的是 introduction,所以转换下-->
<!-- tempObjItem.description = item.introduction || ''-->
<!-- }-->
<!-- } else if (this.objData.type === 'text') {-->
<!-- this.objData.content = item.content || ''-->
<!-- }-->
<!-- // 最终设置到临时缓存-->
<!-- this.tempObj.set(this.objData.type, tempObjItem)-->
<!-- },-->
<!-- openMaterial() {-->
<!-- if (this.objData.type === 'news') {-->
<!-- this.dialogNewsVisible = true-->
<!-- } else if (this.objData.type === 'image') {-->
<!-- this.dialogImageVisible = true-->
<!-- } else if (this.objData.type === 'voice') {-->
<!-- this.dialogVoiceVisible = true-->
<!-- } else if (this.objData.type === 'video') {-->
<!-- this.dialogVideoVisible = true-->
<!-- } else if (this.objData.type === 'music') {-->
<!-- this.dialogThumbVisible = true-->
<!-- }-->
<!-- },-->
<!-- closeMaterial() {-->
<!-- this.dialogNewsVisible = false-->
<!-- this.dialogImageVisible = false-->
<!-- this.dialogVoiceVisible = false-->
<!-- this.dialogVideoVisible = false-->
<!-- this.dialogThumbVisible = false-->
<!-- },-->
<!-- deleteObj() {-->
<!-- if (this.objData.type === 'news') {-->
<!-- this.$delete(this.objData, 'articles')-->
<!-- } else if (this.objData.type === 'image') {-->
<!-- this.objData.mediaId = null-->
<!-- this.$delete(this.objData, 'url')-->
<!-- this.objData.name = null-->
<!-- } else if (this.objData.type === 'voice') {-->
<!-- this.objData.mediaId = null-->
<!-- this.$delete(this.objData, 'url')-->
<!-- this.objData.name = null-->
<!-- } else if (this.objData.type === 'video') {-->
<!-- this.objData.mediaId = null-->
<!-- this.$delete(this.objData, 'url')-->
<!-- this.objData.name = null-->
<!-- this.objData.title = null-->
<!-- this.objData.description = null-->
<!-- } else if (this.objData.type === 'music') {-->
<!-- this.objData.thumbMediaId = null-->
<!-- this.objData.thumbMediaUrl = null-->
<!-- this.objData.title = null-->
<!-- this.objData.description = null-->
<!-- this.objData.musicUrl = null-->
<!-- this.objData.hqMusicUrl = null-->
<!-- } else if (this.objData.type === 'text') {-->
<!-- this.objData.content = null-->
<!-- }-->
<!-- // 覆盖缓存-->
<!-- this.tempObj.set(this.objData.type, Object.assign({}, this.objData))-->
<!-- },-->
<!-- /**-->
<!-- * 输入时,缓存每次 objData 到 tempObj 中-->
<!-- *-->
<!-- * why?不确定为什么 v-model="objData.content" 不能自动缓存,所以通过这样的方式-->
<!-- */-->
<!-- inputContent(str) {-->
<!-- // 覆盖缓存-->
<!-- this.tempObj.set(this.objData.type, Object.assign({}, this.objData))-->
<!-- }-->
<!-- }-->
<!--}-->
<!--</script>-->
<!--<style lang="scss" scoped>--> .select-item {
<!--.public-account-management {--> width: 280px;
<!-- .el-input {--> padding: 10px;
<!-- width: 70%;--> margin: 0 auto 10px auto;
<!-- margin-right: 2%;--> border: 1px solid #eaeaea;
<!-- }--> }
<!--}-->
<!--.pagination {--> .select-item2 {
<!-- text-align: right;--> padding: 10px;
<!-- margin-right: 25px;--> margin: 0 auto 10px auto;
<!--}--> border: 1px solid #eaeaea;
<!--.select-item {--> }
<!-- width: 280px;-->
<!-- padding: 10px;--> .ope-row {
<!-- margin: 0 auto 10px auto;--> padding-top: 10px;
<!-- border: 1px solid #eaeaea;--> text-align: center;
<!--}--> }
<!--.select-item2 {-->
<!-- padding: 10px;--> .input-margin-bottom {
<!-- margin: 0 auto 10px auto;--> margin-bottom: 2%;
<!-- border: 1px solid #eaeaea;--> }
<!--}-->
<!--.ope-row {--> .item-name {
<!-- padding-top: 10px;--> font-size: 12px;
<!-- text-align: center;--> overflow: hidden;
<!--}--> text-overflow: ellipsis;
<!--.item-name {--> white-space: nowrap;
<!-- font-size: 12px;--> text-align: center;
<!-- overflow: hidden;--> }
<!-- text-overflow: ellipsis;-->
<!-- white-space: nowrap;--> .el-form-item__content {
<!-- text-align: center;--> line-height: unset !important;
<!--}--> }
<!--.el-form-item__content {-->
<!-- line-height: unset !important;--> .col-select {
<!--}--> border: 1px solid rgb(234, 234, 234);
<!--.col-select {--> padding: 50px 0px;
<!-- border: 1px solid rgb(234, 234, 234);--> height: 160px;
<!-- padding: 50px 0px;--> width: 49.5%;
<!-- height: 160px;--> }
<!-- width: 49.5%;-->
<!--}--> .col-select2 {
<!--.col-select2 {--> border: 1px solid rgb(234, 234, 234);
<!-- border: 1px solid rgb(234, 234, 234);--> padding: 50px 0px;
<!-- padding: 50px 0px;--> height: 160px;
<!-- height: 160px;--> }
<!--}-->
<!--.col-add {--> .col-add {
<!-- border: 1px solid rgb(234, 234, 234);--> border: 1px solid rgb(234, 234, 234);
<!-- padding: 50px 0px;--> padding: 50px 0px;
<!-- height: 160px;--> height: 160px;
<!-- width: 49.5%;--> width: 49.5%;
<!-- float: right;--> float: right;
<!--}--> }
<!--.avatar-uploader-icon {-->
<!-- border: 1px solid #d9d9d9;--> .avatar-uploader-icon {
<!-- font-size: 28px;--> border: 1px solid #d9d9d9;
<!-- color: #8c939d;--> font-size: 28px;
<!-- width: 100px !important;--> color: #8c939d;
<!-- height: 100px !important;--> width: 100px !important;
<!-- line-height: 100px !important;--> height: 100px !important;
<!-- text-align: center;--> line-height: 100px !important;
<!--}--> text-align: center;
<!--.material-img {--> }
<!-- width: 100%;-->
<!--}--> .material-img {
<!--.thumb-div {--> width: 100%;
<!-- display: inline-block;--> }
<!-- text-align: center;-->
<!--}--> .thumb-div {
<!--.item-infos {--> display: inline-block;
<!-- width: 30%;--> text-align: center;
<!-- margin: auto;--> }
<!--}-->
<!--</style>--> .item-infos {
width: 30%;
margin: auto;
}
</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()">
<el-icon> <!-- 提示 -->
<VideoPlay /> <div>
</el-icon> <Icon icon="ep:video-play" class="mr-5px" />
<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"
width="40%"
append-to-body
@close="closeDialog"
>
<video-player <video-player
v-if="playerOptions.sources[0].src" v-if="dialogVideo"
class="video-player vjs-custom-skin" class="video-player vjs-big-play-centered"
ref="videoPlayerRef" :src="url"
:playsinline="true" poster=""
:options="playerOptions" crossorigin="anonymous"
@play="onPlayerPlay($event)" playsinline
@pause="onPlayerPause($event)" controls
:volume="0.6"
:height="320"
:playback-rates="[0.7, 1.0, 1.5, 2.0]"
/> />
</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({
components: {
VideoPlayer
},
props: {
url: { url: {
// 视频地址,例如说:https://www.iocoder.cn/xxx.mp4 // 视频地址,例如说:https://vjs.zencdn.net/v/oceans.mp4
type: String, type: String,
required: true required: true
} }
}) },
const videoPlayerRef = ref() setup() {
const dialogVideo = ref(false) // const videoPlayerRef = ref(null)
const playerOptions = reactive({ const dialogVideo = ref(false)
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: '', // 你的封面地址
width: document.documentElement.clientWidth,
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖 Video.js 无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true,
durationDisplay: true,
remainingTimeDisplay: false,
fullscreenToggle: true //全屏按钮
}
})
const playVideo = () => { const handleEvent = (log) => {
console.log('Basic player event', log)
}
const playVideo = () => {
dialogVideo.value = true dialogVideo.value = true
playerOptions.sources[0].src = props.url }
}
const closeDialog = () => {
// 暂停播放
// 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%">
<template #footer>
<wx-msg :user-id="userId" v-if="open" /> <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()
......
<template> <template>
<span>开发中</span> <!-- 搜索工作栏 -->
<content-wrap>
<el-form class="-mb-15px" ref="queryForm" :inline="true" label-width="68px">
<el-form-item label="公众号" prop="accountId">
<el-select v-model="accountId" @change="getSummary" class="!w-240px">
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="dateRange">
<el-date-picker
v-model="dateRange"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
@change="getSummary"
class="!w-240px"
/>
</el-form-item>
</el-form>
</content-wrap>
<!-- 图表 -->
<content-wrap>
<el-row>
<el-col :span="12" class="card-box">
<el-card>
<template #header>
<div>
<span>用户增减数据</span>
</div>
</template>
<Echart :options="userSummaryOption" :height="420" />
</el-card>
</el-col>
<el-col :span="12" class="card-box">
<el-card>
<template #header>
<div>
<span>累计用户数据</span>
</div>
</template>
<Echart :options="userCumulateOption" :height="420" />
</el-card>
</el-col>
<el-col :span="12" class="card-box">
<el-card>
<template #header>
<div>
<span>消息概况数据</span>
</div>
</template>
<Echart :options="upstreamMessageOption" :height="420" />
</el-card>
</el-col>
<el-col :span="12" class="card-box">
<el-card>
<template #header>
<div>
<span>接口分析数据</span>
</div>
</template>
<Echart :options="interfaceSummaryOption" :height="420" />
</el-card>
</el-col>
</el-row>
</content-wrap>
</template> </template>
<script setup lang="ts" name="MpStatistics">
import { formatDate, addTime, betweenDay, beginOfDay, endOfDay } from '@/utils/formatTime'
import * as StatisticsApi from '@/api/mp/statistics'
import * as MpAccountApi from '@/api/mp/account'
const message = useMessage() // 消息弹窗
// 默认开始时间是当前日期-7,结束时间是当前日期-1
const dateRange = ref([
beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)),
endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
])
const accountId = ref() // 选中的公众号编号
const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
const xAxisDate = ref([] as any[]) // X 轴的日期范围
// 用户增减数据图表配置项
const userSummaryOption = reactive({
color: ['#67C23A', '#E5323E'],
legend: {
data: ['新增用户', '取消关注的用户']
},
tooltip: {},
xAxis: {
data: [] as any[] // X 轴的日期范围
},
yAxis: {
minInterval: 1
},
series: [
{
name: '新增用户',
type: 'bar',
label: {
show: true
},
barGap: 0,
data: [] as any[] // 新增用户的数据
},
{
name: '取消关注的用户',
type: 'bar',
label: {
show: true
},
data: [] as any[] // 取消关注的用户的数据
}
]
})
// 累计用户数据图表配置项
const userCumulateOption = reactive({
legend: {
data: ['累计用户量']
},
xAxis: {
type: 'category',
data: [] as any[]
},
yAxis: {
minInterval: 1
},
series: [
{
name: '累计用户量',
data: [] as any[], // 累计用户量的数据
type: 'line',
smooth: true,
label: {
show: true
}
}
]
})
// 消息发送概况数据图表配置项
const upstreamMessageOption = reactive({
color: ['#67C23A', '#E5323E'],
legend: {
data: ['用户发送人数', '用户发送条数']
},
tooltip: {},
xAxis: {
data: [] as any[] // X 轴的日期范围
},
yAxis: {
minInterval: 1
},
series: [
{
name: '用户发送人数',
type: 'line',
smooth: true,
label: {
show: true
},
data: [] as any[] // 用户发送人数的数据
},
{
name: '用户发送条数',
type: 'line',
smooth: true,
label: {
show: true
},
data: [] as any[] // 用户发送条数的数据
}
]
})
// 接口分析况数据图表配置项
const interfaceSummaryOption = reactive({
color: ['#67C23A', '#E5323E', '#E6A23C', '#409EFF'],
legend: {
data: ['被动回复用户消息的次数', '失败次数', '最大耗时', '总耗时']
},
tooltip: {},
xAxis: {
data: [] as any[] // X 轴的日期范围
},
yAxis: {},
series: [
{
name: '被动回复用户消息的次数',
type: 'bar',
label: {
show: true
},
barGap: 0,
data: [] as any[] // 被动回复用户消息的次数的数据
},
{
name: '失败次数',
type: 'bar',
label: {
show: true
},
data: [] as any[] // 失败次数的数据
},
{
name: '最大耗时',
type: 'bar',
label: {
show: true
},
data: [] as any[] // 最大耗时的数据
},
{
name: '总耗时',
type: 'bar',
label: {
show: true
},
data: [] as any[] // 总耗时的数据
}
]
})
/** 加载公众号账号的列表 */
const getAccountList = async () => {
accountList.value = await MpAccountApi.getSimpleAccountList()
// 默认选中第一个
if (accountList.value.length > 0) {
accountId.value = accountList.value[0].id
}
}
/** 加载数据 */
const getSummary = () => {
// 如果没有选中公众号账号,则进行提示。
if (!accountId) {
message.error('未选中公众号,无法统计数据')
return false
}
// 必须选择 7 天内,因为公众号有时间跨度限制为 7
if (betweenDay(dateRange.value[0], dateRange.value[1]) >= 7) {
message.error('时间间隔 7 天以内,请重新选择')
return false
}
// 清空横坐标日期
xAxisDate.value = []
// 横坐标加载日期数据
const days = betweenDay(dateRange.value[0], dateRange.value[1]) // 相差天数
for (let i = 0; i <= days; i++) {
xAxisDate.value.push(
formatDate(addTime(dateRange.value[0], 3600 * 1000 * 24 * i), 'YYYY-MM-DD')
)
}
// 初始化图表
initUserSummaryChart()
initUserCumulateChart()
initUpstreamMessageChart()
interfaceSummaryChart()
}
/** 用户增减数据 */
const initUserSummaryChart = async () => {
userSummaryOption.xAxis.data = []
userSummaryOption.series[0].data = []
userSummaryOption.series[1].data = []
try {
// 用户增减数据
const data = await StatisticsApi.getUserSummary({
accountId: accountId.value,
date: [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])]
})
// 横坐标
userSummaryOption.xAxis.data = xAxisDate.value
// 处理数据
xAxisDate.value.forEach((date, index) => {
data.forEach((item) => {
// 匹配日期
const refDate = formatDate(new Date(item.refDate), 'YYYY-MM-DD')
if (refDate.indexOf(date) === -1) {
return
}
// 设置数据到对应的位置
userSummaryOption.series[0].data[index] = item.newUser
userSummaryOption.series[1].data[index] = item.cancelUser
})
})
} catch {}
}
/** 累计用户数据 */
const initUserCumulateChart = async () => {
userCumulateOption.xAxis.data = []
userCumulateOption.series[0].data = []
// 发起请求
try {
const data = await StatisticsApi.getUserCumulate({
accountId: accountId.value,
date: [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])]
})
userCumulateOption.xAxis.data = xAxisDate.value
// 处理数据
data.forEach((item, index) => {
userCumulateOption.series[0].data[index] = item.cumulateUser
})
} catch {}
}
/** 消息概况数据 */
const initUpstreamMessageChart = async () => {
upstreamMessageOption.xAxis.data = []
upstreamMessageOption.series[0].data = []
upstreamMessageOption.series[1].data = []
// 发起请求
try {
const data = await StatisticsApi.getUpstreamMessage({
accountId: accountId.value,
date: [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])]
})
upstreamMessageOption.xAxis.data = xAxisDate.value
// 处理数据
data.forEach((item, index) => {
upstreamMessageOption.series[0].data[index] = item.messageUser
upstreamMessageOption.series[1].data[index] = item.messageCount
})
} catch {}
}
/** 接口分析数据 */
const interfaceSummaryChart = async () => {
interfaceSummaryOption.xAxis.data = []
interfaceSummaryOption.series[0].data = []
interfaceSummaryOption.series[1].data = []
interfaceSummaryOption.series[2].data = []
interfaceSummaryOption.series[3].data = []
// 发起请求
try {
const data = await StatisticsApi.getInterfaceSummary({
accountId: accountId.value,
date: [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])]
})
interfaceSummaryOption.xAxis.data = xAxisDate.value
// 处理数据
data.forEach((item, index) => {
interfaceSummaryOption.series[0].data[index] = item.callbackCount
interfaceSummaryOption.series[1].data[index] = item.failCount
interfaceSummaryOption.series[2].data[index] = item.maxTimeCost
interfaceSummaryOption.series[3].data[index] = item.totalTimeCost
})
} catch {}
}
/** 初始化 */
onMounted(async () => {
// 获取公众号下拉列表
await getAccountList()
// 加载数据
getSummary()
})
</script>
...@@ -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>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="用户编号" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="用户类型" prop="userType">
<el-select
v-model="queryParams.userType"
placeholder="请选择用户类型"
clearable
class="!w-240px"
>
<el-option
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>
<!-- 列表 --> <!-- 列表 -->
<XTable @register="registerTable"> <content-wrap>
<template #actionbtns_default="{ row }"> <el-table v-loading="loading" :data="list">
<!-- 操作:详情 --> <el-table-column label="编号" align="center" prop="id" />
<XTextButton <el-table-column label="用户类型" align="center" prop="userType">
preIcon="ep:view" <template #default="scope">
:title="t('action.detail')" <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
v-hasPermi="['system:notify-message:query']" </template>
@click="handleDetail(row.id)" </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> </template>
</XTable> </el-table-column>
</ContentWrap> <el-table-column label="是否已读" align="center" prop="readStatus" width="100">
<!-- 弹窗 --> <template #default="scope">
<XModal id="messageModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle"> <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.readStatus" />
<!-- 表单:详情 --> </template>
<Descriptions </el-table-column>
v-if="actionType === 'detail'" <el-table-column
:schema="allSchemas.detailSchema" label="阅读时间"
:data="detailData" align="center"
prop="readTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/> />
<template #footer> <el-table-column label="操作" align="center" fixed="right">
<!-- 按钮:关闭 --> <template #default="scope">
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" /> <el-button
link
type="primary"
@click="openDetail(scope.row)"
v-hasPermi="['system:notify-message:query']"
>
详情
</el-button>
</template> </template>
</XModal> </el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</content-wrap>
<!-- 表单弹窗:详情 -->
<NotifyMessageDetail ref="detailRef" />
</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 setDialogTile = (type: string) => { const handleQuery = () => {
modelLoading.value = true queryParams.pageNo = 1
modelTitle.value = t('action.' + type) getList()
actionType.value = type
modelVisible.value = true
} }
// 详情操作 /** 重置按钮操作 */
const handleDetail = async (rowId: number) => { const resetQuery = () => {
setDialogTile('detail') queryFormRef.value.resetFields()
const res = await NotifyMessageApi.getNotifyMessageApi(rowId) handleQuery()
detailData.value = res
modelLoading.value = false
} }
/** 详情操作 */
const detailRef = ref()
const openDetail = (data: NotifyMessageApi.NotifyMessageVO) => {
detailRef.value.open(data)
}
/** 初始化 **/
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)
<template>
<Dialog title="消息详情" v-model="modelVisible" :scroll="true" :max-height="500">
<el-descriptions border :column="1">
<el-descriptions-item label="发送人">
{{ detailData.templateNickname }}
</el-descriptions-item>
<el-descriptions-item label="发送时间">
{{ formatDate(detailData.createTime, 'YYYY-MM-DD HH:mm:ss') }}
</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="阅读时间" v-if="detailData.readStatus">
{{ formatDate(detailData.readTime, 'YYYY-MM-DD HH:mm:ss') }}
</el-descriptions-item>
<el-descriptions-item label="内容">
{{ detailData.templateContent }}
</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>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="是否已读" prop="readStatus">
<el-select
v-model="queryParams.readStatus"
placeholder="请选择状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
: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-button @click="handleUpdateList">
<Icon icon="ep:reading" class="mr-5px" /> 标记已读
</el-button>
<el-button @click="handleUpdateAll">
<Icon icon="ep:reading" class="mr-5px" /> 全部已读
</el-button>
</el-form-item>
</el-form>
</content-wrap>
<content-wrap>
<!-- 列表 --> <!-- 列表 -->
<XTable @register="registerTable"> <el-table
<template #toolbar_buttons> v-loading="loading"
<!-- 操作:标记已读 --> :data="list"
<XButton type="primary" preIcon="ep:zoom-in" title="标记已读" @click="handleUpdateList" /> ref="tableRef"
<!-- 操作:全部已读 --> row-key="id"
<XButton type="primary" preIcon="ep:zoom-in" title="全部已读" @click="handleUpdateAll" /> @selection-change="handleSelectionChange"
>
<el-table-column type="selection" :selectable="selectable" :reserve-selection="true" />
<el-table-column label="发送人" align="center" prop="templateNickname" width="180" />
<el-table-column
label="发送时间"
align="center"
prop="createTime"
width="200"
:formatter="dateFormatter"
/>
<el-table-column label="类型" align="center" prop="templateType" width="180">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="scope.row.templateType" />
</template> </template>
<template #actionbtns_default="{ row }"> </el-table-column>
<!-- 操作:已读 --> <el-table-column
<XTextButton label="消息内容"
preIcon="ep:view" align="center"
title="已读" prop="templateContent"
v-hasPermi="['system:notify-message:query']" show-overflow-tooltip
v-if="!row.readStatus"
@click="handleUpdate([row.id])"
/> />
<el-table-column label="是否已读" align="center" prop="readStatus" width="160">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.readStatus" />
</template> </template>
</XTable> </el-table-column>
</ContentWrap> <el-table-column
label="阅读时间"
align="center"
prop="readTime"
width="200"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-button
link
:type="scope.row.readStatus ? 'primary' : 'warning'"
@click="openDetail(scope.row)"
>
{{ scope.row.readStatus ? '详情' : '已读' }}
</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>
<!-- 表单弹窗:详情 -->
<MyNotifyMessageDetail ref="detailRef" />
</template> </template>
<script setup lang="ts" name="MyNotifyMessage"> <script setup lang="ts" name="MyNotifyMessage">
// 业务相关的 import import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
import { allSchemas } from './my.data' import { dateFormatter } from '@/utils/formatTime'
import * as NotifyMessageApi from '@/api/system/notify/message' import * as NotifyMessageApi from '@/api/system/notify/message'
import MyNotifyMessageDetail from './MyNotifyMessageDetail.vue'
const message = useMessage() // 消息 const message = useMessage() // 消息
// 列表相关的变量 const loading = ref(true) // 列表的加载中
const [registerTable, { reload, getCheckboxRecords }] = useXTable({ const total = ref(0) // 列表的总页数
allSchemas: allSchemas, const list = ref([]) // 列表的数据
getListApi: NotifyMessageApi.getMyNotifyMessagePage const queryParams = reactive({
pageNo: 1,
pageSize: 10,
readStatus: undefined,
createTime: []
}) })
const queryFormRef = ref() // 搜索的表单
const tableRef = ref() // 表格的 Ref
const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组
const handleUpdateList = async () => { /** 查询列表 */
const list = getCheckboxRecords() as any as any[] const getList = async () => {
if (list.length === 0) { loading.value = true
return try {
const data = await NotifyMessageApi.getMyNotifyMessagePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
tableRef.value.clearSelection()
handleQuery()
}
/** 详情操作 */
const detailRef = ref()
const openDetail = (data: NotifyMessageApi.NotifyMessageVO) => {
if (!data.readStatus) {
handleReadOne(data.id)
} }
await handleUpdate(list.map((v) => v.id)) detailRef.value.open(data)
} }
// 标记指定 id 已读 /** 标记一条站内信已读 */
const handleUpdate = async (ids) => { const handleReadOne = async (id) => {
await NotifyMessageApi.updateNotifyMessageRead(ids) await NotifyMessageApi.updateNotifyMessageRead(id)
message.success('标记已读成功!') await getList()
reload()
} }
// 标记全部已读 /** 标记全部站内信已读 **/
const handleUpdateAll = async () => { const handleUpdateAll = async () => {
await NotifyMessageApi.updateAllNotifyMessageRead() await NotifyMessageApi.updateAllNotifyMessageRead()
message.success('全部已读成功!') message.success('全部已读成功!')
reload() tableRef.value.clearSelection()
await getList()
}
/** 标记一些站内信已读 **/
const handleUpdateList = async () => {
if (selectedIds.value.length === 0) {
return
}
await NotifyMessageApi.updateNotifyMessageRead(selectedIds.value)
message.success('批量已读成功!')
tableRef.value.clearSelection()
await getList()
} }
/** 某一行,是否允许选中 */
const selectable = (row) => {
return !row.readStatus
}
/** 当表格选择项发生变化时会触发该事件 */
const handleSelectionChange = (array: NotifyMessageApi.NotifyMessageVO[]) => {
selectedIds.value = []
if (!array) {
return
}
array.forEach((row) => selectedIds.value.push(row.id))
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script> </script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryTitle: ' ',
primaryType: 'checkbox',
action: true,
actionWidth: '200', // 3个按钮默认200,如有删减对应增减即可
columns: [
{
title: '发送人名称',
field: 'templateNickname',
table: {
width: 120
}
},
{
title: '发送时间',
field: 'createTime',
isForm: false,
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
},
table: {
width: 180
}
},
{
title: '类型',
field: 'templateType',
dictType: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
dictClass: 'number',
table: {
width: 80
}
},
{
title: '内容',
field: 'templateContent'
},
{
title: '是否已读',
field: 'readStatus',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
dictClass: 'boolean',
table: {
width: 80
},
isSearch: true
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
...@@ -115,7 +115,7 @@ const queryParams = reactive({ ...@@ -115,7 +115,7 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -156,7 +156,7 @@ const queryParams = reactive({ ...@@ -156,7 +156,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 {
......
...@@ -168,7 +168,7 @@ const queryFormRef = ref() // 搜索的表单 ...@@ -168,7 +168,7 @@ const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
const tagList = ref([]) // 标签数组 const tagList = ref([]) // 标签数组
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -146,7 +146,7 @@ const queryParams = reactive({ ...@@ -146,7 +146,7 @@ const queryParams = reactive({
createTime: [] createTime: []
}) })
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -191,7 +191,7 @@ const queryFormRef = ref() // 搜索的表单 ...@@ -191,7 +191,7 @@ const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
const packageList = ref([]) //租户套餐列表 const packageList = ref([]) //租户套餐列表
/** 查询参数列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -43,7 +43,7 @@ const crudSchemas = reactive<VxeCrudSchema>({ ...@@ -43,7 +43,7 @@ const crudSchemas = reactive<VxeCrudSchema>({
{ {
title: t('form.remark'), title: t('form.remark'),
field: 'remark', field: 'remark',
isTable: false, isTable: true,
isSearch: true, isSearch: true,
form: { form: {
component: 'Input', component: 'Input',
......
...@@ -8,12 +8,12 @@ ...@@ -8,12 +8,12 @@
<el-input v-model="formData.nickname" :disabled="true" /> <el-input v-model="formData.nickname" :disabled="true" />
</el-form-item> </el-form-item>
<el-form-item label="角色"> <el-form-item label="角色">
<el-select v-model="formData.roleIds" multiple placeholder="请选择"> <el-select v-model="formData.roleIds" multiple placeholder="请选择角色">
<el-option <el-option
v-for="item in roleOptions" v-for="item in roleOptions"
:key="parseInt(item.id)" :key="item.id"
:label="item.name" :label="item.name"
:value="parseInt(item.id)" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// TODO el-dialog 用 Dialog 组件
import { assignUserRoleApi, PermissionAssignUserRoleReqVO } from '@/api/system/permission' import { assignUserRoleApi, PermissionAssignUserRoleReqVO } from '@/api/system/permission'
interface Props { interface Props {
...@@ -86,5 +87,3 @@ const submit = async () => { ...@@ -86,5 +87,3 @@ const submit = async () => {
} }
} }
</script> </script>
<style></style>
...@@ -269,6 +269,7 @@ ...@@ -269,6 +269,7 @@
import type { ElTree } from 'element-plus' import type { ElTree } from 'element-plus'
import { handleTree, defaultProps } from '@/utils/tree' import { handleTree, defaultProps } from '@/utils/tree'
// 原vue3版本api方法都是Api结尾觉得见名知义,个人觉得这个可以形成规范 // 原vue3版本api方法都是Api结尾觉得见名知义,个人觉得这个可以形成规范
// TODO 使用 DeptApi 这种形式哈
import { getSimpleDeptList as getSimpleDeptListApi } from '@/api/system/dept' import { getSimpleDeptList as getSimpleDeptListApi } from '@/api/system/dept'
import { getSimplePostList as getSimplePostListApi, PostVO } from '@/api/system/post' import { getSimplePostList as getSimplePostListApi, PostVO } from '@/api/system/post'
import { DICT_TYPE, getDictOptions } from '@/utils/dict' import { DICT_TYPE, getDictOptions } from '@/utils/dict'
...@@ -279,16 +280,15 @@ import { ...@@ -279,16 +280,15 @@ import {
updateUserStatusApi, updateUserStatusApi,
UserVO UserVO
} from '@/api/system/user' } from '@/api/system/user'
import { parseTime } from './utils' import { parseTime } from './utils' // TODO 可以使用 formatTime 里的方法
import AddForm from './AddForm.vue' import AddForm from './AddForm.vue' // TODO 改成 UserForm
import ImportForm from './ImportForm.vue' import ImportForm from './ImportForm.vue' // TODO 改成 UserImportForm
import RoleForm from './RoleForm.vue' import RoleForm from './RoleForm.vue' // TODO 改成 UserAssignRoleForm
import { getUserApi, getUserPageApi } from '@/api/system/user' import { getUserApi, getUserPageApi } from '@/api/system/user'
import { getSimpleRoleList as getSimpleRoleListApi } from '@/api/system/role' import { getSimpleRoleList as getSimpleRoleListApi } from '@/api/system/role'
import { listUserRolesApi } from '@/api/system/permission' import { listUserRolesApi } from '@/api/system/permission'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import download from '@/utils/download' import download from '@/utils/download'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
...@@ -304,10 +304,11 @@ const queryParams = reactive({ ...@@ -304,10 +304,11 @@ const queryParams = reactive({
const showSearch = ref(true) const showSearch = ref(true)
const showAddDialog = ref(false) const showAddDialog = ref(false)
// 数据字典- // 数据字典- // TODO 可以直接 vue 那 getIntDictOptions,这样一方面少个变量,也可以 getIntDictOptions
const statusDictDatas = getDictOptions(DICT_TYPE.COMMON_STATUS) const statusDictDatas = getDictOptions(DICT_TYPE.COMMON_STATUS)
// ========== 创建部门树结构 ========== // ========== 创建部门树结构 ==========
// TODO 要不把部门树拆成一个左侧的组件,然后点击后触发 handleDeptNodeClick
const deptName = ref('') const deptName = ref('')
watch( watch(
() => deptName.value, () => deptName.value,
...@@ -375,6 +376,7 @@ const resetQuery = () => { ...@@ -375,6 +376,7 @@ const resetQuery = () => {
// 添加或编辑 // 添加或编辑
const addEditFormRef = ref() const addEditFormRef = ref()
// 添加用户 // 添加用户
// TODO 可以参考别的模块哈,openForm;然后 tree 和 position 可以里面在加载下,让组件自己维护自己哈。
const handleAdd = () => { const handleAdd = () => {
addEditFormRef?.value.resetForm() addEditFormRef?.value.resetForm()
// 获得下拉数据 // 获得下拉数据
...@@ -389,6 +391,7 @@ const handleImport = () => { ...@@ -389,6 +391,7 @@ const handleImport = () => {
} }
// 用户导出 // 用户导出
// TODO 改成 await 的风格;
const exportLoading = ref(false) const exportLoading = ref(false)
const handleExport = () => { const handleExport = () => {
message message
...@@ -432,6 +435,7 @@ const handleCommand = (command: string, index: number, row: UserVO) => { ...@@ -432,6 +435,7 @@ const handleCommand = (command: string, index: number, row: UserVO) => {
} }
// 用户状态修改 // 用户状态修改
// TODO 改成 await 的风格;
const handleStatusChange = (row: UserVO) => { const handleStatusChange = (row: UserVO) => {
let text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用' let text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
message message
...@@ -466,6 +470,7 @@ const handleUpdate = (row: UserVO) => { ...@@ -466,6 +470,7 @@ const handleUpdate = (row: UserVO) => {
} }
// 删除用户 // 删除用户
// TODO 改成 await 的风格;
const handleDelete = (row: UserVO) => { const handleDelete = (row: UserVO) => {
const ids = row.id const ids = row.id
message message
...@@ -481,6 +486,7 @@ const handleDelete = (row: UserVO) => { ...@@ -481,6 +486,7 @@ const handleDelete = (row: UserVO) => {
} }
// 重置密码 // 重置密码
// TODO 改成 await 的风格;
const handleResetPwd = (row: UserVO) => { const handleResetPwd = (row: UserVO) => {
message.prompt('请输入"' + row.username + '"的新密码', t('common.reminder')).then(({ value }) => { message.prompt('请输入"' + row.username + '"的新密码', t('common.reminder')).then(({ value }) => {
resetUserPwdApi(row.id, value) resetUserPwdApi(row.id, value)
......
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