瀏覽代碼

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

fengjingtao 2 年之前
父節點
當前提交
a1609e3046
共有 77 個文件被更改,包括 4823 次插入3039 次删除
  1. 2 0
      .gitignore
  2. 2 0
      package.json
  3. 1 0
      src/api/bpm/processInstance/index.ts
  4. 78 12
      src/api/infra/codegen/index.ts
  5. 0 61
      src/api/infra/codegen/types.ts
  6. 1 1
      src/api/infra/fileConfig/index.ts
  7. 2 20
      src/api/system/notify/message/index.ts
  8. 二進制
      src/assets/imgs/profile.jpg
  9. 二進制
      src/assets/imgs/wechat.png
  10. 4 0
      src/plugins/formCreate/index.ts
  11. 2 2
      src/router/modules/remaining.ts
  12. 2 0
      src/types/auto-components.d.ts
  13. 51 0
      src/utils/formatTime.ts
  14. 1 1
      src/views/bpm/definition/index.vue
  15. 1 1
      src/views/bpm/form/index.vue
  16. 1 1
      src/views/bpm/group/index.vue
  17. 1 1
      src/views/bpm/model/index.vue
  18. 0 154
      src/views/bpm/processInstance/create.vue
  19. 130 0
      src/views/bpm/processInstance/create/index.vue
  20. 0 490
      src/views/bpm/processInstance/detail.vue
  21. 55 0
      src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
  22. 89 0
      src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
  23. 81 0
      src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue
  24. 277 0
      src/views/bpm/processInstance/detail/index.vue
  25. 0 39
      src/views/bpm/processInstance/process.create.ts
  26. 1 1
      src/views/bpm/taskAssignRule/index.vue
  27. 1 1
      src/views/infra/apiErrorLog/index.vue
  28. 64 51
      src/views/infra/codegen/EditTable.vue
  29. 142 0
      src/views/infra/codegen/ImportTable.vue
  30. 42 23
      src/views/infra/codegen/PreviewCode.vue
  31. 0 53
      src/views/infra/codegen/codegen.data.ts
  32. 65 155
      src/views/infra/codegen/components/BasicInfoForm.vue
  33. 0 137
      src/views/infra/codegen/components/CloumInfoForm.vue
  34. 157 0
      src/views/infra/codegen/components/ColumInfoForm.vue
  35. 379 0
      src/views/infra/codegen/components/GenerateInfoForm.vue
  36. 0 123
      src/views/infra/codegen/components/ImportTable.vue
  37. 3 4
      src/views/infra/codegen/components/index.ts
  38. 215 79
      src/views/infra/codegen/index.vue
  39. 1 1
      src/views/infra/config/index.vue
  40. 1 1
      src/views/infra/dataSourceConfig/index.vue
  41. 1 1
      src/views/infra/file/index.vue
  42. 1 1
      src/views/infra/fileConfig/index.vue
  43. 167 62
      src/views/infra/job/JobLog.vue
  44. 74 0
      src/views/infra/job/JobLogView.vue
  45. 172 0
      src/views/infra/job/form.vue
  46. 244 246
      src/views/infra/job/index.vue
  47. 0 69
      src/views/infra/job/job.data.ts
  48. 0 75
      src/views/infra/job/jobLog.data.ts
  49. 44 0
      src/views/infra/job/utils.ts
  50. 89 0
      src/views/infra/job/view.vue
  51. 7 7
      src/views/mp/account/index.vue
  52. 302 0
      src/views/mp/components/wx-material-select/main.vue
  53. 152 155
      src/views/mp/components/wx-msg/main.vue
  54. 706 623
      src/views/mp/components/wx-reply/main.vue
  55. 58 90
      src/views/mp/components/wx-video-play/main.vue
  56. 1 0
      src/views/mp/components/wx-voice-play/main.vue
  57. 16 5
      src/views/mp/freePublish/index.vue
  58. 37 16
      src/views/mp/message/index.vue
  59. 363 1
      src/views/mp/statistics/index.vue
  60. 14 5
      src/views/mp/tag/index.vue
  61. 1 1
      src/views/system/dict/data.vue
  62. 1 1
      src/views/system/loginlog/index.vue
  63. 1 1
      src/views/system/menu/index.vue
  64. 64 0
      src/views/system/notify/message/NotifyMessageDetail.vue
  65. 194 53
      src/views/system/notify/message/index.vue
  66. 0 101
      src/views/system/notify/message/message.data.ts
  67. 46 0
      src/views/system/notify/my/MyNotifyMessageDetail.vue
  68. 194 39
      src/views/system/notify/my/index.vue
  69. 0 58
      src/views/system/notify/my/my.data.ts
  70. 1 1
      src/views/system/oauth2/token/index.vue
  71. 1 1
      src/views/system/operatelog/index.vue
  72. 1 1
      src/views/system/sensitiveWord/index.vue
  73. 1 1
      src/views/system/sms/channel/index.vue
  74. 1 1
      src/views/system/tenant/index.vue
  75. 1 1
      src/views/system/tenantPackage/tenantPackage.data.ts
  76. 4 5
      src/views/system/user/RoleForm.vue
  77. 12 6
      src/views/system/user/index.vue

+ 2 - 0
.gitignore

@@ -6,4 +6,6 @@ dist-ssr
 /dist*
 *-lock.*
 pnpm-debug
+
 .idea
+.history

+ 2 - 0
package.json

@@ -29,6 +29,7 @@
     "@form-create/designer": "^3.1.0",
     "@form-create/element-ui": "^3.1.17",
     "@iconify/iconify": "^3.1.0",
+    "@videojs-player/vue": "^1.0.0",
     "@vueuse/core": "^9.13.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.10",
@@ -58,6 +59,7 @@
     "qs": "^6.11.1",
     "steady-xml": "^0.1.0",
     "url": "^0.11.0",
+    "video.js": "^8.0.4",
     "vue": "3.2.47",
     "vue-i18n": "9.2.2",
     "vue-router": "^4.1.6",

+ 1 - 0
src/api/bpm/processInstance/index.ts

@@ -4,6 +4,7 @@ export type Task = {
   id: string
   name: string
 }
+
 export type ProcessInstanceVO = {
   id: number
   name: string

+ 78 - 12
src/api/infra/codegen/index.ts

@@ -1,57 +1,123 @@
 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 })
 }
 
 // 查询详情代码生成表定义
-export const getCodegenTableApi = (id: number) => {
+export const getCodegenTable = (id: number) => {
   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 })
 }
 
 // 修改代码生成表定义
-export const updateCodegenTableApi = (data: CodegenUpdateReqVO) => {
+export const updateCodegenTable = (data: CodegenUpdateReqVO) => {
   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 })
 }
 
 // 基于 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 })
 }
 
 // 预览生成代码
-export const previewCodegenApi = (id: number) => {
+export const previewCodegen = (id: number) => {
   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 })
 }
 
 // 获得表定义
-export const getSchemaTableListApi = (params) => {
+export const getSchemaTableList = (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 })
 }
 
 // 删除代码生成表定义
-export const deleteCodegenTableApi = (id: number) => {
+export const deleteCodegenTable = (id: number) => {
   return request.delete({ url: '/infra/codegen/delete?tableId=' + id })
 }

+ 0 - 61
src/api/infra/codegen/types.ts

@@ -1,61 +0,0 @@
-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[]
-}

+ 1 - 1
src/api/infra/fileConfig/index.ts

@@ -17,7 +17,7 @@ export interface FileClientConfig {
 export interface FileConfigVO {
   id: number
   name: string
-  storage: number
+  storage: any
   master: boolean
   visible: boolean
   config: FileClientConfig

+ 2 - 20
src/api/system/notify/message/index.ts

@@ -15,31 +15,13 @@ export interface NotifyMessageVO {
   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 })
 }
 
-// 查询站内信消息详情
-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 })
 }
 

二進制
src/assets/imgs/profile.jpg


二進制
src/assets/imgs/wechat.png


+ 4 - 0
src/plugins/formCreate/index.ts

@@ -10,6 +10,8 @@ import {
   ElTransfer,
   ElAlert,
   ElTabs,
+  ElTable,
+  ElTableColumn,
   ElTabPane
 } from 'element-plus'
 
@@ -27,6 +29,8 @@ const components = [
   ElTransfer,
   ElAlert,
   ElTabs,
+  ElTable,
+  ElTableColumn,
   ElTabPane
 ]
 

+ 2 - 2
src/router/modules/remaining.ts

@@ -272,7 +272,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: '/process-instance/create',
-        component: () => import('@/views/bpm/processInstance/create.vue'),
+        component: () => import('@/views/bpm/processInstance/create/index.vue'),
         name: 'BpmProcessInstanceCreate',
         meta: {
           noCache: true,
@@ -284,7 +284,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: '/process-instance/detail',
-        component: () => import('@/views/bpm/processInstance/detail.vue'),
+        component: () => import('@/views/bpm/processInstance/detail/index.vue'),
         name: 'BpmProcessInstanceDetail',
         meta: {
           noCache: true,

+ 2 - 0
src/types/auto-components.d.ts

@@ -68,6 +68,8 @@ declare module '@vue/runtime-core' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     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']
     ElTransfer: typeof import('element-plus/es')['ElTransfer']
     ElTree: typeof import('element-plus/es')['ElTree']

+ 51 - 0
src/utils/formatTime.ts

@@ -196,3 +196,54 @@ export const dateFormatter = (row, column, 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
+}

+ 1 - 1
src/views/bpm/definition/index.vue

@@ -110,7 +110,7 @@ const queryParams = reactive({
   key: query.key
 })
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/bpm/form/index.vue

@@ -103,7 +103,7 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/bpm/group/index.vue

@@ -132,7 +132,7 @@ const queryParams = reactive({
 const queryFormRef = ref() // 搜索的表单
 const userList = ref([]) // 用户列表
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/bpm/model/index.vue

@@ -248,7 +248,7 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 0 - 154
src/views/bpm/processInstance/create.vue

@@ -1,154 +0,0 @@
-<template>
-  <ContentWrap>
-    <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
-    <div v-if="!selectProcessInstance">
-      <XTable @register="registerTable">
-        <!-- 流程分类 -->
-        <template #category_default="{ row }">
-          <DictTag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="Number(row?.category)" />
-        </template>
-        <template #version_default="{ row }">
-          <el-tag v-if="row">v{{ row.version }}</el-tag>
-        </template>
-        <template #actionbtns_default="{ row }">
-          <XTextButton preIcon="ep:plus" title="选择" @click="handleSelect(row)" />
-        </template>
-      </XTable>
-    </div>
-    <!-- 第二步,填写表单,进行流程的提交 -->
-    <div v-else>
-      <el-card class="box-card">
-        <div class="clearfix">
-          <span class="el-icon-document">申请信息【{{ selectProcessInstance.name }}】</span>
-          <XButton
-            style="float: right"
-            type="primary"
-            preIcon="ep:delete"
-            title="选择其它流程"
-            @click="selectProcessInstance = undefined"
-          />
-        </div>
-        <el-col :span="16" :offset="6" style="margin-top: 20px">
-          <form-create
-            :rule="detailForm.rule"
-            v-model:api="fApi"
-            :option="detailForm.option"
-            @submit="submitForm"
-          />
-        </el-col>
-      </el-card>
-      <el-card class="box-card">
-        <div class="clearfix">
-          <span class="el-icon-picture-outline">流程图</span>
-        </div>
-        <!-- TODO 芋艿:待完成??? -->
-        <my-process-viewer
-          key="designer"
-          v-model="bpmnXML"
-          :value="bpmnXML"
-          v-bind="bpmnControlForm"
-          :prefix="bpmnControlForm.prefix"
-        />
-      </el-card>
-    </div>
-  </ContentWrap>
-</template>
-<script setup lang="ts">
-// 业务相关的 import
-import { allSchemas } from './process.create'
-import * as DefinitionApi from '@/api/bpm/definition'
-import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import { setConfAndFields2 } from '@/utils/formCreate'
-import type { ApiAttrs } from '@form-create/element-ui/types/config'
-import { DICT_TYPE } from '@/utils/dict'
-
-const router = useRouter() // 路由
-const message = useMessage() // 消息
-
-// ========== 列表相关 ==========
-
-const [registerTable] = useXTable({
-  allSchemas: allSchemas,
-  params: {
-    suspensionState: 1
-  },
-  getListApi: DefinitionApi.getProcessDefinitionList,
-  isList: true
-})
-
-// ========== 表单相关 ==========
-
-const fApi = ref<ApiAttrs>()
-
-// 流程表单详情
-const detailForm = ref({
-  rule: [],
-  option: {}
-})
-
-// 流程表单
-const selectProcessInstance = ref() // 选择的流程实例
-/** 处理选择流程的按钮操作 **/
-const handleSelect = async (row) => {
-  // 设置选择的流程
-  selectProcessInstance.value = row
-
-  // 情况一:流程表单
-  if (row.formType == 10) {
-    // 设置表单
-    setConfAndFields2(detailForm, row.formConf, row.formFields)
-
-    // 加载流程图
-    DefinitionApi.getProcessDefinitionBpmnXML(row.id).then((response) => {
-      bpmnXML.value = response
-    })
-    // 情况二:业务表单
-  } else if (row.formCustomCreatePath) {
-    await router.push({
-      path: row.formCustomCreatePath
-    })
-    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
-  }
-}
-
-/** 提交按钮 */
-const submitForm = async (formData) => {
-  if (!fApi.value || !selectProcessInstance.value) {
-    return
-  }
-
-  // 提交请求
-  fApi.value.btn.loading(true)
-  try {
-    await ProcessInstanceApi.createProcessInstanceApi({
-      processDefinitionId: selectProcessInstance.value.id,
-      variables: formData
-    })
-    // 提示
-    message.success('发起流程成功')
-    // this.$tab.closeOpenPage();
-    router.go(-1)
-  } finally {
-    fApi.value.btn.loading(false)
-  }
-}
-
-// ========== 流程图相关 ==========
-
-// // BPMN 数据
-const bpmnXML = ref(null)
-const bpmnControlForm = ref({
-  prefix: 'flowable'
-})
-</script>
-
-<style lang="scss">
-.my-process-designer {
-  height: calc(100vh - 200px);
-}
-
-.box-card {
-  width: 100%;
-  margin-bottom: 20px;
-}
-</style>

+ 130 - 0
src/views/bpm/processInstance/create/index.vue

@@ -0,0 +1,130 @@
+<template>
+  <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
+  <ContentWrap v-if="!selectProcessInstance">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="流程名称" align="center" prop="name" />
+      <el-table-column label="流程分类" align="center" prop="category">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
+        </template>
+      </el-table-column>
+      <el-table-column label="流程版本" align="center" prop="version">
+        <template #default="scope">
+          <el-tag>v{{ scope.row.version }}</el-tag>
+        </template>
+      </el-table-column>
+      <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>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 第二步,填写表单,进行流程的提交 -->
+  <ContentWrap v-else>
+    <el-card class="box-card">
+      <div class="clearfix">
+        <span class="el-icon-document">申请信息【{{ selectProcessInstance.name }}】</span>
+        <el-button style="float: right" type="primary" @click="selectProcessInstance = undefined">
+          <Icon icon="ep:delete" /> 选择其它流程
+        </el-button>
+      </div>
+      <el-col :span="16" :offset="6" style="margin-top: 20px">
+        <form-create
+          :rule="detailForm.rule"
+          v-model:api="fApi"
+          :option="detailForm.option"
+          @submit="submitForm"
+        />
+      </el-col>
+    </el-card>
+    <!-- 流程图预览 -->
+    <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML" />
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import * as DefinitionApi from '@/api/bpm/definition'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+const router = useRouter() // 路由
+const message = useMessage() // 消息
+
+// ========== 列表相关 ==========
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  suspensionState: 1
+})
+
+/** 查询列表 */
+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 detailForm = ref({
+  // 流程表单详情
+  rule: [],
+  option: {}
+})
+const selectProcessInstance = ref() // 选择的流程实例
+
+/** 处理选择流程的按钮操作 **/
+const handleSelect = async (row) => {
+  // 设置选择的流程
+  selectProcessInstance.value = row
+
+  // 情况一:流程表单
+  if (row.formType == 10) {
+    // 设置表单
+    setConfAndFields2(detailForm, row.formConf, row.formFields)
+    // 加载流程图
+    bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
+    // 情况二:业务表单
+  } else if (row.formCustomCreatePath) {
+    await router.push({
+      path: row.formCustomCreatePath
+    })
+    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
+  }
+}
+
+/** 提交按钮 */
+const submitForm = async (formData) => {
+  if (!fApi.value || !selectProcessInstance.value) {
+    return
+  }
+  // 提交请求
+  fApi.value.btn.loading(true)
+  try {
+    await ProcessInstanceApi.createProcessInstanceApi({
+      processDefinitionId: selectProcessInstance.value.id,
+      variables: formData
+    })
+    // 提示
+    message.success('发起流程成功')
+    router.go(-1)
+  } finally {
+    fApi.value.btn.loading(false)
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 0 - 490
src/views/bpm/processInstance/detail.vue

@@ -1,490 +0,0 @@
-<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>

+ 55 - 0
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue

@@ -0,0 +1,55 @@
+<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>

+ 89 - 0
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue

@@ -0,0 +1,89 @@
+<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>

+ 81 - 0
src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue

@@ -0,0 +1,81 @@
+<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>

+ 277 - 0
src/views/bpm/processInstance/detail/index.vue

@@ -0,0 +1,277 @@
+<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>

+ 0 - 39
src/views/bpm/processInstance/process.create.ts

@@ -1,39 +0,0 @@
-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)

+ 1 - 1
src/views/bpm/taskAssignRule/index.vue

@@ -56,7 +56,7 @@ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
 const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/infra/apiErrorLog/index.vue

@@ -182,7 +182,7 @@ const queryParams = reactive({
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 64 - 51
src/views/infra/codegen/EditTable.vue

@@ -1,67 +1,80 @@
 <template>
-  <ContentWrap>
-    <ContentDetailWrap :title="title" @back="push('/infra/codegen')">
-      <el-tabs v-model="activeName">
-        <el-tab-pane label="基本信息" name="basicInfo">
-          <BasicInfoForm ref="basicInfoRef" :basicInfo="tableCurrentRow" />
-        </el-tab-pane>
-        <el-tab-pane label="字段信息" name="cloum">
-          <CloumInfoForm ref="cloumInfoRef" :info="cloumCurrentRow" />
-        </el-tab-pane>
-      </el-tabs>
-      <template #right>
-        <XButton
-          type="primary"
-          :title="t('action.save')"
-          :loading="loading"
-          @click="submitForm()"
-        />
-      </template>
-    </ContentDetailWrap>
-  </ContentWrap>
+  <content-wrap v-loading="formLoading">
+    <el-tabs v-model="activeName">
+      <el-tab-pane label="基本信息" name="basicInfo">
+        <basic-info-form ref="basicInfoRef" :table="formData.table" />
+      </el-tab-pane>
+      <el-tab-pane label="字段信息" name="colum">
+        <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-tabs>
+    <el-form>
+      <el-form-item style="float: right">
+        <el-button type="primary" @click="submitForm" :loading="formLoading">保存</el-button>
+        <el-button @click="close">返回</el-button>
+      </el-form-item>
+    </el-form>
+  </content-wrap>
 </template>
 <script setup lang="ts">
-import { BasicInfoForm, CloumInfoForm } from './components'
-import { getCodegenTableApi, updateCodegenTableApi } from '@/api/infra/codegen'
-import { CodegenTableVO, CodegenColumnVO, CodegenUpdateReqVO } from '@/api/infra/codegen/types'
-
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { BasicInfoForm, ColumInfoForm, GenerateInfoForm } from './components'
+import * as CodegenApi from '@/api/infra/codegen'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const { push } = useRouter()
-const { query } = useRoute()
-const loading = ref(false)
-const title = ref('代码生成')
-const activeName = ref('basicInfo')
-const cloumInfoRef = ref(null)
-const tableCurrentRow = ref<CodegenTableVO>()
-const cloumCurrentRow = ref<CodegenColumnVO[]>([])
+const { push, currentRoute } = useRouter() // 路由
+const { query } = useRoute() // 查询参数
+const { delView } = useTagsViewStore() // 视图操作
+
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const activeName = ref('basicInfo') // Tag 激活的窗口
 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
-  if (id) {
-    // 获取表详细信息
-    const res = await getCodegenTableApi(id)
-    title.value = '修改[ ' + res.table.tableName + ' ]生成配置'
-    tableCurrentRow.value = res.table
-    cloumCurrentRow.value = res.columns
+  if (!id) {
+    return
+  }
+  formLoading.value = true
+  try {
+    formData.value = await CodegenApi.getCodegenTable(id)
+  } finally {
+    formLoading.value = false
   }
 }
+
+/** 提交按钮 */
 const submitForm = async () => {
-  const basicInfo = unref(basicInfoRef)
-  const basicForm = await basicInfo?.elFormRef?.validate()?.catch(() => {})
-  if (basicForm) {
-    const basicInfoData = (await basicInfo?.getFormData()) as CodegenTableVO
-    const genTable: CodegenUpdateReqVO = {
-      table: basicInfoData,
-      columns: cloumCurrentRow.value
-    }
-    await updateCodegenTableApi(genTable)
+  // 参数校验
+  if (!unref(formData)) return
+  await unref(basicInfoRef)?.validate()
+  await unref(generateInfoRef)?.validate()
+  try {
+    // 提交请求
+    await CodegenApi.updateCodegenTable(formData.value)
     message.success(t('common.updateSuccess'))
-    push('/infra/codegen')
-  }
+    close()
+  } catch {}
+}
+
+/** 关闭按钮 */
+const close = () => {
+  delView(unref(currentRoute))
+  push('/infra/codegen')
 }
+
+/** 初始化 */
 onMounted(() => {
-  getList()
+  getDetail()
 })
 </script>

+ 142 - 0
src/views/infra/codegen/ImportTable.vue

@@ -0,0 +1,142 @@
+<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>

+ 42 - 23
src/views/infra/codegen/components/Preview.vue → src/views/infra/codegen/PreviewCode.vue

@@ -1,5 +1,11 @@
 <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">
       <el-card class="w-1/4" :gutter="12" shadow="hover">
         <el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
@@ -10,6 +16,7 @@
             :expand-on-click-node="false"
             highlight-current
             @node-click="handleNodeClick"
+            default-expand-all
           />
         </el-scrollbar>
       </el-card>
@@ -21,38 +28,34 @@
             :name="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>
           </el-tab-pane>
         </el-tabs>
       </el-card>
     </div>
-  </XModal>
+  </Dialog>
 </template>
 <script setup lang="ts">
 import { useClipboard } from '@vueuse/core'
 import { handleTree2 } from '@/utils/tree'
-import { previewCodegenApi } from '@/api/infra/codegen'
-import { CodegenTableVO, CodegenPreviewVO } from '@/api/infra/codegen/types'
+import * as CodegenApi from '@/api/infra/codegen'
+import { CodegenPreviewVO } from '@/api/infra/codegen/types'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
+
+const modelVisible = ref(false) // 弹窗的是否展示
+const modelTitle = ref('代码预览') // 弹窗的标题
 // ======== 显示页面 ========
 const preview = reactive({
-  open: false,
-  titel: '代码预览',
   fileTree: [],
   activeName: ''
 })
 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) => {
   if (node && !node.isLeaf) {
     return false
@@ -132,14 +135,30 @@ const copy = async (text: string) => {
   const { copy, copied, isSupported } = useClipboard({ source: text })
   if (!isSupported) {
     message.error(t('common.copyError'))
-  } else {
-    await copy()
-    if (unref(copied)) {
-      message.success(t('common.copySuccess'))
-    }
+    return
+  }
+  await copy()
+  if (unref(copied)) {
+    message.success(t('common.copySuccess'))
   }
 }
-defineExpose({
-  show
-})
+
+/** 打开弹窗 */
+const openModal = async (id: number) => {
+  modelVisible.value = true
+  const res = await CodegenApi.previewCodegen(id)
+  let file = handleFiles(res)
+  previewCodegen.value = res
+  preview.fileTree = handleTree2(file, 'id', 'parentId', 'children', '/')
+  preview.activeName = res[0].filePath
+}
+defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
 </script>
+<style lang="scss">
+.app-infra-codegen-preview-container {
+  .el-scrollbar .el-scrollbar__wrap .el-scrollbar__view {
+    white-space: nowrap;
+    display: inline-block;
+  }
+}
+</style>

+ 0 - 53
src/views/infra/codegen/codegen.data.ts

@@ -1,53 +0,0 @@
-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)

+ 65 - 155
src/views/infra/codegen/components/BasicInfoForm.vue

@@ -1,183 +1,93 @@
 <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>
 <script setup lang="ts">
-import { useForm } from '@/hooks/web/useForm'
-import { FormSchema } from '@/types/form'
 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'
 
+const emits = defineEmits(['update:basicInfo'])
 const props = defineProps({
-  basicInfo: {
+  table: {
     type: Object as PropType<Nullable<CodegenTableVO>>,
     default: () => null
   }
 })
 
-const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
-const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
-const menuOptions = ref<any>([]) // 树形结构
-const getTree = async () => {
-  const res = await getSimpleMenusList()
-  menuOptions.value = handleTree(res)
-}
+const formRef = ref()
+const formData = ref({
+  tableName: '',
+  tableComment: '',
+  className: '',
+  author: '',
+  remark: ''
+})
 
 const rules = reactive({
   tableName: [required],
   tableComment: [required],
   className: [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
+  author: [required]
 })
+
 watch(
-  () => props.basicInfo,
-  (basicInfo) => {
-    if (!basicInfo) return
-    const { setValues } = methods
-    setValues(basicInfo)
+  () => props.table,
+  (table) => {
+    if (!table) return
+    formData.value = table
   },
   {
     deep: true,
     immediate: true
   }
 )
-// ========== 初始化 ==========
-onMounted(async () => {
-  await getTree()
-})
-
+watch(
+  () => formData.value,
+  (val) => {
+    emits('update:basicInfo', val)
+  }
+)
 defineExpose({
-  elFormRef,
-  getFormData: methods.getFormData
+  validate: async () => unref(formRef)?.validate()
 })
 </script>

+ 0 - 137
src/views/infra/codegen/components/CloumInfoForm.vue

@@ -1,137 +0,0 @@
-<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>

+ 157 - 0
src/views/infra/codegen/components/ColumInfoForm.vue

@@ -0,0 +1,157 @@
+<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>

+ 379 - 0
src/views/infra/codegen/components/GenerateInfoForm.vue

@@ -0,0 +1,379 @@
+<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>

+ 0 - 123
src/views/infra/codegen/components/ImportTable.vue

@@ -1,123 +0,0 @@
-<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>

+ 3 - 4
src/views/infra/codegen/components/index.ts

@@ -1,5 +1,4 @@
 import BasicInfoForm from './BasicInfoForm.vue'
-import CloumInfoForm from './CloumInfoForm.vue'
-import ImportTable from './ImportTable.vue'
-import Preview from './Preview.vue'
-export { BasicInfoForm, CloumInfoForm, ImportTable, Preview }
+import ColumInfoForm from './ColumInfoForm.vue'
+import GenerateInfoForm from './GenerateInfoForm.vue'
+export { BasicInfoForm, ColumInfoForm, GenerateInfoForm }

+ 215 - 79
src/views/infra/codegen/index.vue

@@ -1,107 +1,243 @@
 <template>
-  <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <template #toolbar_buttons>
-        <!-- 操作:导入 -->
-        <XButton
-          type="primary"
-          preIcon="ep:zoom-in"
-          :title="t('action.import')"
-          v-hasPermi="['infra:codegen:create']"
-          @click="openImportTable()"
+  <!-- 搜索 -->
+  <content-wrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="表名称" prop="tableName">
+        <el-input
+          v-model="queryParams.tableName"
+          placeholder="请输入表名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
         />
-      </template>
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:预览 -->
-        <XTextButton
-          preIcon="ep:view"
-          :title="t('action.preview')"
-          v-hasPermi="['infra:codegen:query']"
-          @click="handlePreview(row)"
+      </el-form-item>
+      <el-form-item label="表描述" prop="tableComment">
+        <el-input
+          v-model="queryParams.tableComment"
+          placeholder="请输入表描述"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
         />
-        <!-- 操作:编辑 -->
-        <XTextButton
-          preIcon="ep:edit"
-          :title="t('action.edit')"
-          v-hasPermi="['infra:codegen:update']"
-          @click="handleUpdate(row.id)"
+      </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"
         />
-        <!-- 操作:删除 -->
-        <XTextButton
-          preIcon="ep:delete"
-          :title="t('action.del')"
-          v-hasPermi="['infra:codegen:delete']"
-          @click="deleteData(row.id)"
-        />
-        <!-- 操作:同步 -->
-        <XTextButton
-          preIcon="ep:refresh"
-          :title="t('action.sync')"
-          v-hasPermi="['infra:codegen:update']"
-          @click="handleSynchDb(row)"
-        />
-        <!-- 操作:生成 -->
-        <XTextButton
-          preIcon="ep:download"
-          :title="t('action.generate')"
-          v-hasPermi="['infra:codegen:download']"
-          @click="handleGenTable(row)"
-        />
-      </template>
-    </XTable>
-  </ContentWrap>
+      </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>
+      </el-table-column>
+      <el-table-column label="表名称" align="center" prop="tableName" width="200" />
+      <el-table-column
+        label="表描述"
+        align="center"
+        prop="tableComment"
+        :show-overflow-tooltip="true"
+        width="200"
+      />
+      <el-table-column label="实体" align="center" prop="className" width="200" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="更新时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center" width="300px" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="handlePreview(scope.row)"
+            v-hasPermi="['infra:codegen:preview']"
+          >
+            预览
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleUpdate(scope.row.id)"
+            v-hasPermi="['infra:codegen:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:codegen:delete']"
+          >
+            删除
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleSyncDB(scope.row)"
+            v-hasPermi="['infra:codegen:update']"
+          >
+            同步
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleGenTable(scope.row)"
+            v-hasPermi="['infra:codegen:download']"
+          >
+            生成代码
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </content-wrap>
+
   <!-- 弹窗:导入表 -->
-  <ImportTable ref="importRef" @ok="reload()" />
+  <ImportTable ref="importRef" success="getList" />
   <!-- 弹窗:预览代码 -->
-  <Preview ref="previewRef" />
+  <PreviewCode ref="previewRef" />
 </template>
 <script setup lang="ts" name="Codegen">
+import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as CodegenApi from '@/api/infra/codegen'
-import { CodegenTableVO } from '@/api/infra/codegen/types'
-import { allSchemas } from './codegen.data'
-import { ImportTable, Preview } from './components'
-
-const { t } = useI18n() // 国际化
+import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
+import ImportTable from './ImportTable.vue'
+import PreviewCode from './PreviewCode.vue'
 const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
 const { push } = useRouter() // 路由跳转
-// 列表相关的变量
-const [registerTable, { reload, deleteData }] = useXTable({
-  allSchemas: allSchemas,
-  getListApi: CodegenApi.getCodegenTablePageApi,
-  deleteApi: CodegenApi.deleteCodegenTableApi
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+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 openImportTable = () => {
-  importRef.value.show()
+  importRef.value.open()
+}
+
+/** 编辑操作 */
+const handleUpdate = (id: number) => {
+  push('/codegen/edit?id=' + id)
 }
-// 预览操作
+
+/** 预览操作 */
 const previewRef = ref()
-const handlePreview = (row: CodegenTableVO) => {
-  previewRef.value.show(row)
+const handlePreview = (row: CodegenApi.CodegenTableVO) => {
+  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 同步
   const tableName = row.tableName
-  message
-    .confirm('确认要强制同步' + tableName + '表结构吗?', t('common.reminder'))
-    .then(async () => {
-      await CodegenApi.syncCodegenFromDBApi(row.id)
-      message.success('同步成功')
-    })
+  try {
+    await message.confirm('确认要强制同步' + tableName + '表结构吗?', t('common.reminder'))
+    await CodegenApi.syncCodegenFromDB(row.id)
+    message.success('同步成功')
+  } catch {}
 }
 
-// 生成代码操作
-const handleGenTable = async (row: CodegenTableVO) => {
-  const res = await CodegenApi.downloadCodegenApi(row.id)
+/** 生成代码操作 */
+const handleGenTable = async (row: CodegenApi.CodegenTableVO) => {
+  const res = await CodegenApi.downloadCodegen(row.id)
   download.zip(res, 'codegen-' + row.className + '.zip')
 }
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  // 加载数据源列表
+  dataSourceConfigList.value = await DataSourceConfigApi.getDataSourceConfigList()
+})
 </script>

+ 1 - 1
src/views/infra/config/index.vue

@@ -153,7 +153,7 @@ const queryParams = reactive({
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/infra/dataSourceConfig/index.vue

@@ -66,7 +66,7 @@ const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const list = ref([]) // 列表的数据
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/infra/file/index.vue

@@ -104,7 +104,7 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/infra/fileConfig/index.vue

@@ -130,7 +130,7 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 167 - 62
src/views/infra/job/JobLog.vue

@@ -1,74 +1,179 @@
 <template>
-  <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <template #toolbar_buttons>
-        <XButton
-          type="warning"
-          preIcon="ep:download"
-          :title="t('action.export')"
-          v-hasPermi="['infra:job:export']"
-          @click="exportList('定时任务详情.xls')"
+  <content-wrap>
+    <!-- 搜索栏 -->
+    <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="120px">
+      <el-form-item label="处理器的名字" prop="handlerName">
+        <el-input
+          v-model="queryParams.handlerName"
+          placeholder="请输入处理器的名字"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="开始执行时间" prop="beginTime">
+        <el-date-picker
+          clearable
+          v-model="queryParams.beginTime"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="选择开始执行时间"
         />
-      </template>
-      <template #beginTime_default="{ row }">
-        <span>{{ parseTime(row.beginTime) + ' ~ ' + parseTime(row.endTime) }}</span>
-      </template>
-      <template #duration_default="{ row }">
-        <span>{{ row.duration + ' 毫秒' }}</span>
-      </template>
-      <template #actionbtns_default="{ row }">
-        <XTextButton
-          preIcon="ep:view"
-          :title="t('action.detail')"
-          v-hasPermi="['infra:job:query']"
-          @click="handleDetail(row)"
+      </el-form-item>
+      <el-form-item label="结束执行时间" prop="endTime">
+        <el-date-picker
+          clearable
+          v-model="queryParams.endTime"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="选择结束执行时间"
         />
-      </template>
-    </XTable>
-  </ContentWrap>
-  <XModal v-model="dialogVisible" :title="dialogTitle">
-    <!-- 对话框(详情) -->
-    <Descriptions :schema="allSchemas.detailSchema" :data="detailData">
-      <template #retryInterval="{ row }">
-        <span>{{ row.retryInterval + '毫秒' }} </span>
-      </template>
-      <template #monitorTimeout="{ row }">
-        <span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span>
-      </template>
-    </Descriptions>
-    <!-- 操作按钮 -->
-    <template #footer>
-      <XButton :title="t('dialog.close')" @click="dialogVisible = false" />
-    </template>
-  </XModal>
+      </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>
+      </el-table-column>
+      <el-table-column label="执行时长" align="center" prop="duration">
+        <template #default="scope">
+          <span>{{ scope.row.duration + ' 毫秒' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="任务状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            link
+            icon="el-icon-view"
+            @click="handleView(scope.row.id)"
+            :loading="exportLoading"
+            v-hasPermi="['infra:job:query']"
+            >详细
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </content-wrap>
+  <!-- 表单弹窗:查看 -->
+  <log-view ref="viewModalRef" @success="getList" />
 </template>
-<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 { allSchemas } from './jobLog.data'
+import { parseTime } from './utils'
 
-const { t } = useI18n() // 国际化
-// 列表相关的变量
-const [registerTable, { exportList }] = useXTable({
-  allSchemas: allSchemas,
-  getListApi: JobLogApi.getJobLogPageApi,
-  exportListApi: JobLogApi.exportJobLogApi
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  handlerName: undefined,
+  beginTime: undefined,
+  endTime: undefined,
+  status: undefined
 })
-// ========== CRUD 相关 ==========
-const dialogVisible = ref(false) // 是否显示弹出层
-const dialogTitle = ref('') // 弹出层标题
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
 
-// ========== 详情相关 ==========
-const detailData = ref() // 详情 Ref
+/** 查询参数列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await JobLogApi.getJobLogPageApi({
+      ...queryParams,
+      beginTime: queryParams.beginTime ? queryParams.beginTime + ' 00:00:00' : undefined,
+      endTime: queryParams.endTime ? queryParams.endTime + ' 23:59:59' : undefined
+    })
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
 
-// 详情操作
-const handleDetail = async (row: JobLogApi.JobLogVO) => {
-  // 设置数据
-  const res = await JobLogApi.getJobLogApi(row.id)
-  detailData.value = res
-  dialogTitle.value = t('action.detail')
-  dialogVisible.value = true
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
 }
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 查看操作 */
+const viewModalRef = ref()
+const handleView = (rowId?: number) => {
+  viewModalRef.value.openModal(rowId)
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await JobLogApi.exportJobLogApi(queryParams)
+    download.excel(data, '定时任务执行日志.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
 </script>

+ 74 - 0
src/views/infra/job/JobLogView.vue

@@ -0,0 +1,74 @@
+<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>

+ 172 - 0
src/views/infra/job/form.vue

@@ -0,0 +1,172 @@
+<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>

+ 244 - 246
src/views/infra/job/index.vue

@@ -1,243 +1,175 @@
 <template>
-  <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <template #toolbar_buttons>
-        <!-- 操作:新增 -->
-        <XButton
+  <content-wrap>
+    <!-- 搜索栏 -->
+    <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px">
+      <el-form-item label="任务名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入任务名称"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+      </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_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="处理器的名字" prop="handlerName">
+        <el-input
+          v-model="queryParams.handlerName"
+          placeholder="请输入处理器的名字"
+          clearable
+          @keyup.enter="handleQuery"
+        />
+      </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"
-          preIcon="ep:zoom-in"
-          :title="t('action.add')"
+          plain
+          @click="openModal('create')"
           v-hasPermi="['infra:job:create']"
-          @click="handleCreate()"
-        />
-        <!-- 操作:导出 -->
-        <XButton
-          type="warning"
-          preIcon="ep:download"
-          :title="t('action.export')"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
           v-hasPermi="['infra:job:export']"
-          @click="exportList('定时任务.xls')"
-        />
-        <XButton
-          type="info"
-          preIcon="ep:zoom-in"
-          title="执行日志"
-          v-hasPermi="['infra:job:query']"
-          @click="handleJobLog()"
-        />
-      </template>
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:修改 -->
-        <XTextButton
-          preIcon="ep:edit"
-          :title="t('action.edit')"
-          v-hasPermi="['infra:job:update']"
-          @click="handleUpdate(row.id)"
-        />
-        <XTextButton
-          preIcon="ep:edit"
-          :title="row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停'"
-          v-hasPermi="['infra:job:update']"
-          @click="handleChangeStatus(row)"
-        />
-        <!-- 操作:删除 -->
-        <XTextButton
-          preIcon="ep:delete"
-          :title="t('action.del')"
-          v-hasPermi="['infra:job:delete']"
-          @click="deleteData(row.id)"
-        />
-        <el-dropdown class="p-0.5" v-hasPermi="['infra:job:trigger', 'infra:job:query']">
-          <XTextButton :title="t('action.more')" postIcon="ep:arrow-down" />
-          <template #dropdown>
-            <el-dropdown-menu>
-              <el-dropdown-item>
-                <!-- 操作:执行 -->
-                <XTextButton
-                  preIcon="ep:view"
-                  title="执行一次"
-                  v-hasPermi="['infra:job:trigger']"
-                  @click="handleRun(row)"
-                />
-              </el-dropdown-item>
-              <el-dropdown-item>
-                <!-- 操作:详情 -->
-                <XTextButton
-                  preIcon="ep:view"
-                  :title="t('action.detail')"
-                  v-hasPermi="['infra:job:query']"
-                  @click="handleDetail(row.id)"
-                />
-              </el-dropdown-item>
-              <el-dropdown-item>
-                <!-- 操作:日志 -->
-                <XTextButton
-                  preIcon="ep:view"
-                  title="调度日志"
-                  v-hasPermi="['infra:job:query']"
-                  @click="handleJobLog(row.id)"
-                />
-              </el-dropdown-item>
-            </el-dropdown-menu>
-          </template>
-        </el-dropdown>
-      </template>
-    </XTable>
-  </ContentWrap>
-  <XModal v-model="dialogVisible" :title="dialogTitle">
-    <!-- 对话框(添加 / 修改) -->
-    <Form
-      v-if="['create', 'update'].includes(actionType)"
-      :schema="allSchemas.formSchema"
-      :rules="rules"
-      ref="formRef"
-    >
-      <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()"
-      />
-      <!-- 按钮:关闭 -->
-      <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
-    </template>
-  </XModal>
+        >
+          <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']"
+            >修改</el-button
+          >
+          <el-button
+            link
+            icon="el-icon-check"
+            @click="handleChangeStatus(scope.row)"
+            v-hasPermi="['infra:job:update']"
+            >{{ scope.row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停' }}</el-button
+          >
+          <el-button
+            link
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['infra:job:delete']"
+            >删除</el-button
+          >
+          <el-dropdown
+            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>
+              <el-dropdown-menu>
+                <el-dropdown-item command="handleRun" v-if="hasPermi(['infra:job:trigger'])">
+                  执行一次
+                </el-dropdown-item>
+                <el-dropdown-item command="handleView" v-if="hasPermi(['infra:job:query'])">
+                  任务详细
+                </el-dropdown-item>
+                <el-dropdown-item command="handleJobLog" v-if="hasPermi(['infra:job:query'])">
+                  调度日志
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </content-wrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <job-form ref="modalRef" @success="getList" />
+  <!-- 表单弹窗:查看 -->
+  <job-view ref="viewModalRef" @success="getList" />
 </template>
+
 <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 { rules, allSchemas } from './job.data'
 import { InfraJobStatusEnum } from '@/utils/constants'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const { push } = useRouter()
 
-// 列表相关的变量
-const [registerTable, { reload, deleteData, exportList }] = useXTable({
-  allSchemas: allSchemas,
-  getListApi: JobApi.getJobPageApi,
-  deleteApi: JobApi.deleteJobApi,
-  exportListApi: JobApi.exportJobApi
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  handlerName: undefined
 })
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
 
-// ========== CRUD 相关 ==========
-const actionLoading = ref(false) // 遮罩层
-const actionType = ref('') // 操作按钮的类型
-const dialogVisible = ref(false) // 是否显示弹出层
-const dialogTitle = ref('edit') // 弹出层标题
-const formRef = ref<FormExpose>() // 表单 Ref
-const detailData = ref() // 详情 Ref
-const nextTimes = ref([])
-const shortcuts = ref([
-  {
-    text: '每天8点和12点 (自定义追加)',
-    value: '0 0 8,12 * * ?'
+/** 查询参数列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await JobApi.getJobPageApi(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
   }
-])
-// 设置标题
-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 text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
+
   const status =
     row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP
   message
@@ -249,7 +181,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
           : InfraJobStatusEnum.STOP
       await JobApi.updateJobStatusApi(row.id, status)
       message.success(text + '成功')
-      await reload()
+      await getList()
     })
     .catch(() => {
       row.status =
@@ -258,6 +190,43 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
           : 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) => {
   if (rowId) {
@@ -271,32 +240,61 @@ const handleRun = (row: JobApi.JobVO) => {
   message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder')).then(async () => {
     await JobApi.runJobApi(row.id)
     message.success('执行成功')
-    await reload()
+    await getList()
   })
 }
-// 提交按钮
-const submitForm = async () => {
-  const elForm = unref(formRef)?.getElFormRef()
-  if (!elForm) return
-  elForm.validate(async (valid) => {
-    if (valid) {
-      actionLoading.value = true
-      // 提交请求
-      try {
-        const data = unref(formRef)?.formModel as JobApi.JobVO
-        if (actionType.value === 'create') {
-          await JobApi.createJobApi(data)
-          message.success(t('common.createSuccess'))
-        } else {
-          await JobApi.updateJobApi(data)
-          message.success(t('common.updateSuccess'))
-        }
-        dialogVisible.value = false
-      } finally {
-        actionLoading.value = false
-        await reload()
-      }
-    }
-  })
+
+/** '更多'操作按钮 */
+const handleCommand = (command, row) => {
+  switch (command) {
+    case 'handleRun':
+      handleRun(row)
+      break
+    case 'handleView':
+      handleView(row?.id)
+      break
+    case 'handleJobLog':
+      handleJobLog(row?.id)
+      break
+    default:
+      break
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await JobApi.exportJobApi(queryParams)
+    download.excel(data, '定时任务.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+// 权限判断: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>

+ 0 - 69
src/views/infra/job/job.data.ts

@@ -1,69 +0,0 @@
-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)

+ 0 - 75
src/views/infra/job/jobLog.data.ts

@@ -1,75 +0,0 @@
-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)

+ 44 - 0
src/views/infra/job/utils.ts

@@ -0,0 +1,44 @@
+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
+}

+ 89 - 0
src/views/infra/job/view.vue

@@ -0,0 +1,89 @@
+<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>

+ 7 - 7
src/views/mp/account/index.vue

@@ -121,13 +121,13 @@ const queryFormRef = ref() // 搜索的表单
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
-  // 处理查询参数
-  let params = { ...queryParams }
-  // 执行查询
-  const data = await AccountApi.getAccountPage(params)
-  list.value = data.list
-  total.value = data.total
-  loading.value = false
+  try {
+    const data = await AccountApi.getAccountPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
 }
 
 /** 搜索按钮操作 */

+ 302 - 0
src/views/mp/components/wx-material-select/main.vue

@@ -0,0 +1,302 @@
+<!--
+  - 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>

+ 152 - 155
src/views/mp/components/wx-msg/main.vue

@@ -6,7 +6,7 @@
   ② 代码优化,补充注释,提升阅读性
 -->
 <template>
-  <div class="msg-main">
+  <ContentWrap>
     <div class="msg-div" :id="'msg-div' + nowStr">
       <!-- 加载更多 -->
       <div v-loading="loading"></div>
@@ -26,9 +26,9 @@
               :src="item.sendFrom === 1 ? user.avatar : mp.avatar"
               class="avue-comment__avatar"
             />
-            <div class="avue-comment__author">{{
-              item.sendFrom === 1 ? user.nickname : mp.nickname
-            }}</div>
+            <div class="avue-comment__author"
+              >{{ item.sendFrom === 1 ? user.nickname : mp.nickname }}
+            </div>
           </div>
           <div class="avue-comment__main">
             <div class="avue-comment__header">
@@ -40,37 +40,41 @@
             >
               <!-- 【事件】区域 -->
               <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 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 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 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 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 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 v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'">
-                <el-tag size="mini">系统拍照发图</el-tag>
+                <el-tag>系统拍照发图</el-tag>
               </div>
               <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 v-else-if="item.type === 'event' && item.event === 'pic_weixin'">
-                <el-tag size="mini">微信相册</el-tag>
+                <el-tag>微信相册</el-tag>
               </div>
               <div v-else-if="item.type === 'event' && item.event === 'location_select'">
-                <el-tag size="mini">选择地理位置</el-tag>
+                <el-tag>选择地理位置</el-tag>
               </div>
               <div v-else-if="item.type === 'event'">
-                <el-tag type="danger" size="mini">未知事件类型</el-tag>
+                <el-tag type="danger">未知事件类型</el-tag>
               </div>
               <!-- 【消息】区域 -->
               <div v-else-if="item.type === 'text'">{{ item.content }}</div>
@@ -124,10 +128,10 @@
       <wx-reply-select ref="replySelect" :objData="objData" />
       <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
     </div>
-  </div>
+  </ContentWrap>
 </template>
 
-<script>
+<script lang="ts" name="WxMsg">
 import { getMessagePage, sendMessage } from '@/api/mp/message'
 import WxReplySelect from '@/views/mp/components/wx-reply/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'
 import WxLocation from '@/views/mp/components/wx-location/main.vue'
 import WxMusic from '@/views/mp/components/wx-music/main.vue'
 import { getUser } from '@/api/mp/mpuser'
+import { defineComponent } from 'vue'
+
+const message = useMessage() // 消息弹窗
+import profile from '@/assets/imgs/profile.jpg'
+import wechat from '@/assets/imgs/wechat.png'
+import { parseTime } from '@/utils/formatTime'
 
-export default {
-  name: 'WxMsg',
+export default defineComponent({
   components: {
     WxReplySelect,
     WxVideoPlayer,
@@ -153,160 +162,144 @@ export default {
       required: true
     }
   },
-  data() {
-    return {
-      nowStr: new Date().getTime(), // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
-      loading: false, // 消息列表是否正在加载中
-      loadMore: true, // 是否可以加载更多
-      list: [], // 消息列表
-      queryParams: {
-        pageNo: 1, // 当前页数
-        pageSize: 14, // 每页显示多少条
-        accountId: undefined
-      },
-      user: {
-        // 由于微信不再提供昵称,直接使用“用户”展示
-        nickname: '用户',
-        avatar: require('@/assets/images/profile.jpg'),
-        accountId: 0 // 公众号账号编号
-      },
-      mp: {
-        nickname: '公众号',
-        avatar: require('@/assets/images/wechat.png')
-      },
-
-      // ========= 消息发送 =========
-      sendLoading: false, // 发送消息是否加载中
-      objData: {
-        // 微信发送消息
-        type: 'text'
-      }
-    }
-  },
-  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
+  setup(props) {
+    const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
+    const loading = ref(false) // 消息列表是否正在加载中
+    const loadMore = ref(true) // 是否可以加载更多
+    const list = ref<any[]>([]) // 消息列表
+    const queryParams = reactive({
+      pageNo: 1, // 当前页数
+      pageSize: 14, // 每页显示多少条
+      accountId: undefined
+    })
+    const user = reactive({
+      // 由于微信不再提供昵称,直接使用“用户”展示
+      nickname: '用户',
+      avatar: profile,
+      accountId: 0 // 公众号账号编号
+    })
+    const mp = reactive({
+      nickname: '公众号',
+      avatar: wechat
+    })
 
-      // 加载消息
-      console.log(this.queryParams)
-      this.refreshChange()
+    // ========= 消息发送 =========
+    const sendLoading = ref(false) // 发送消息是否加载中
+    const objData = reactive({
+      // 微信发送消息
+      type: 'text',
+      accountId: null,
+      articles: []
     })
-  },
-  methods: {
-    sendMsg() {
-      if (!this.objData) {
+
+    const replySelect = ref(null)
+    // 执行发送
+    const sendMsg = async () => {
+      if (!objData) {
         return
       }
-      // 公众号限制:客服消息,公众号只允许发送一条
-      if (this.objData.type === 'news' && this.objData.articles.length > 1) {
-        this.objData.articles = [this.objData.articles[0]]
-        this.$message({
-          showClose: true,
-          message: '图文消息条数限制在 1 条以内,已默认发送第一条',
-          type: 'success'
-        })
+      //     // 公众号限制:客服消息,公众号只允许发送一条
+      if (objData.type === 'news' && objData.articles.length > 1) {
+        objData.articles = [objData.articles[0]]
+        message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
       }
-
-      // 执行发送
-      this.sendLoading = true
-      sendMessage(
-        Object.assign(
-          {
-            userId: this.userId
-          },
-          {
-            ...this.objData
-          }
-        )
-      )
-        .then((response) => {
-          this.sendLoading = false
-          // 添加到消息列表,并滚动
-          this.list = [...this.list, ...[response.data]]
-          this.scrollToBottom()
-          // 重置 objData 状态
-          this.$refs['replySelect'].deleteObj() // 重置,避免 tab 的数据未清理
-        })
-        .catch(() => {
-          this.sendLoading = false
-        })
-    },
-    loadingMore() {
-      this.queryParams.pageNo++
-      this.getPage(this.queryParams)
-    },
-    getPage(page, params) {
-      this.loading = true
-      getMessagePage(
+      let data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
+      sendLoading.value = false
+      list.value = [...list.value, ...[data]]
+      scrollToBottom()
+      //ts檢查的時候會判斷這個組件可能是空的,所以需要進行斷言。
+      //避免 tab 的数据未清理
+      const deleteObj = (replySelect.value as any).deleteObj
+      if (deleteObj) {
+        deleteObj()
+      }
+    }
+    const loadingMore = () => {
+      queryParams.pageNo++
+      getPage(queryParams, null)
+    }
+    const getPage = async (page, params) => {
+      loading.value = true
+      let dataTemp = await getMessagePage(
         Object.assign(
           {
             pageNo: page.pageNo,
             pageSize: page.pageSize,
-            userId: this.userId,
+            userId: props.userId,
             accountId: page.accountId
           },
           params
         )
-      ).then((response) => {
-        // 计算当前的滚动高度
-        const msgDiv = document.getElementById('msg-div' + this.nowStr)
-        let scrollHeight = 0
-        if (msgDiv) {
-          scrollHeight = msgDiv.scrollHeight
-        }
-
-        // 处理数据
-        const data = response.data.list.reverse()
-        this.list = [...data, ...this.list]
-        this.loading = false
-        if (data.length < this.queryParams.pageSize || data.length === 0) {
-          this.loadMore = false
-        }
-        this.queryParams.pageNo = page.pageNo
-        this.queryParams.pageSize = page.pageSize
-
-        // 滚动到原来的位置
-        if (this.queryParams.pageNo === 1) {
-          // 定位到消息底部
-          this.scrollToBottom()
-        } else if (data.length !== 0) {
-          // 定位滚动条
-          this.$nextTick(() => {
-            if (scrollHeight !== 0) {
-              msgDiv.scrollTop =
-                document.getElementById('msg-div' + this.nowStr).scrollHeight - scrollHeight - 100
+      )
+      const msgDiv = document.getElementById('msg-div' + nowStr.value)
+      let scrollHeight = 0
+      if (msgDiv) {
+        scrollHeight = msgDiv.scrollHeight
+      }
+      // 处理数据
+      let data = dataTemp.list.reverse()
+      list.value = [...data, ...list.value]
+      loading.value = false
+      if (data.length < queryParams.pageSize || data.length === 0) {
+        loadMore.value = false
+      }
+      queryParams.pageNo = page.pageNo
+      queryParams.pageSize = page.pageSize
+      // 滚动到原来的位置
+      if (queryParams.pageNo === 1) {
+        // 定位到消息底部
+        scrollToBottom()
+      } else if (data.length !== 0) {
+        // 定位滚动条
+        await nextTick(() => {
+          if (scrollHeight !== 0) {
+            let div = document.getElementById('msg-div' + nowStr.value)
+            if (div && msgDiv) {
+              msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
             }
-          })
-        }
-      })
-    },
-    /**
-     * 刷新回调
-     */
-    refreshChange() {
-      this.getPage(this.queryParams)
-    },
+          }
+        })
+      }
+    }
+    const refreshChange = () => {
+      getPage(queryParams, null)
+    }
     /** 定位到消息底部 */
-    scrollToBottom: function () {
-      this.$nextTick(() => {
-        let div = document.getElementById('msg-div' + this.nowStr)
-        div.scrollTop = div.scrollHeight
+    const scrollToBottom = () => {
+      nextTick(() => {
+        let div = document.getElementById('msg-div' + nowStr.value)
+        if (div) {
+          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>
 <style lang="scss" scoped>
 /* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc  */
@@ -317,6 +310,7 @@ export default {
   margin-top: -30px;
   padding: 10px;
 }
+
 .msg-div {
   height: 50vh;
   overflow: auto;
@@ -324,13 +318,16 @@ export default {
   margin-left: 10px;
   margin-right: 10px;
 }
+
 .msg-send {
   padding: 10px;
 }
+
 .avatar-div {
   text-align: center;
   width: 80px;
 }
+
 .send-but {
   float: right;
   margin-top: 8px !important;

+ 706 - 623
src/views/mp/components/wx-reply/main.vue

@@ -1,634 +1,717 @@
-<!--&lt;!&ndash;-->
-<!--  - Copyright (C) 2018-2019-->
-<!--  - All rights reserved, Designed By www.joolun.com-->
-<!--  芋道源码:-->
-<!--  ① 移除多余的 rep 为前缀的变量,让 message 消息更简单-->
-<!--  ② 代码优化,补充注释,提升阅读性-->
-<!--  ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入-->
-<!--  ④ 支持发送【视频】消息时,支持新建视频-->
-<!--&ndash;&gt;-->
-<!--<template>-->
-<!--  <el-tabs type="border-card" v-model="objData.type" @tab-click="handleClick">-->
-<!--    &lt;!&ndash; 类型 1:文本 &ndash;&gt;-->
-<!--    <el-tab-pane name="text">-->
-<!--      <span slot="label"><i class="el-icon-document"></i> 文本</span>-->
-<!--      <el-input-->
-<!--        type="textarea"-->
-<!--        :rows="5"-->
-<!--        placeholder="请输入内容"-->
-<!--        v-model="objData.content"-->
-<!--        @input="inputContent"-->
-<!--      />-->
-<!--    </el-tab-pane>-->
-<!--    &lt;!&ndash; 类型 2:图片 &ndash;&gt;-->
-<!--    <el-tab-pane name="image">-->
-<!--      <span slot="label"><i class="el-icon-picture"></i> 图片</span>-->
-<!--      <el-row>-->
-<!--        &lt;!&ndash; 情况一:已经选择好素材、或者上传好图片 &ndash;&gt;-->
-<!--        <div class="select-item" v-if="objData.url">-->
-<!--          <img class="material-img" :src="objData.url" />-->
-<!--          <p class="item-name" v-if="objData.name">{{ objData.name }}</p>-->
-<!--          <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-else>-->
-<!--          <el-row style="text-align: center">-->
-<!--            &lt;!&ndash; 选择素材 &ndash;&gt;-->
-<!--            <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="dialogImageVisible"-->
-<!--                width="90%"-->
-<!--                append-to-body-->
-<!--              >-->
-<!--                <wx-material-select :obj-data="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="beforeImageUpload"-->
-<!--                :on-success="handleUploadSuccess"-->
-<!--              >-->
-<!--                <el-button type="primary">上传图片</el-button>-->
-<!--                <div slot="tip" class="el-upload__tip"-->
-<!--                  >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div-->
-<!--                >-->
-<!--              </el-upload>-->
-<!--            </el-col>-->
-<!--          </el-row>-->
-<!--        </div>-->
-<!--      </el-row>-->
-<!--    </el-tab-pane>-->
-<!--    &lt;!&ndash; 类型 3:语音 &ndash;&gt;-->
-<!--    <el-tab-pane name="voice">-->
-<!--      <span slot="label"><i class="el-icon-phone"></i> 语音</span>-->
-<!--      <el-row>-->
-<!--        <div class="select-item2" v-if="objData.url">-->
-<!--          <p class="item-name">{{ objData.name }}</p>-->
-<!--          <div class="item-infos">-->
-<!--            <wx-voice-player :url="objData.url" />-->
-<!--          </div>-->
-<!--          <el-row class="ope-row">-->
-<!--            <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />-->
-<!--          </el-row>-->
-<!--        </div>-->
-<!--        <div v-else>-->
-<!--          <el-row style="text-align: center">-->
-<!--            &lt;!&ndash; 选择素材 &ndash;&gt;-->
-<!--            <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>-->
+<!--
+  - Copyright (C) 2018-2019
+  - All rights reserved, Designed By www.joolun.com
+  芋道源码:
+  ① 移除多余的 rep 为前缀的变量,让 message 消息更简单
+  ② 代码优化,补充注释,提升阅读性
+  ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入
+  ④ 支持发送【视频】消息时,支持新建视频
+-->
+<template>
+  <el-tabs type="border-card" v-model="objDataRef.type" @tab-click="handleClick">
+    <!-- 类型 1:文本 -->
+    <el-tab-pane name="text">
+      <template #label>
+        <el-row align="middle">
+          <icon icon="ep:document" />
+          文本
+        </el-row>
+      </template>
+      <el-input
+        type="textarea"
+        :rows="5"
+        placeholder="请输入内容"
+        v-model="objDataRef.content"
+        @input="inputContent"
+      />
+    </el-tab-pane>
+    <!-- 类型 2:图片 -->
+    <el-tab-pane name="image">
+      <template #label>
+        <el-row align="middle">
+          <icon icon="ep:picture" class="mr-5px" />
+          图片
+        </el-row>
+      </template>
+      <!-- 情况一:已经选择好素材、或者上传好图片 -->
+      <div class="select-item" v-if="objDataRef.url">
+        <img class="material-img" :src="objDataRef.url" />
+        <p class="item-name" v-if="objDataRef.name">{{ objDataRef.name }}</p>
+        <el-row class="ope-row">
+          <el-button type="danger" circle @click="deleteObj">
+            <icon icon="ep:delete" />
+          </el-button>
+        </el-row>
+      </div>
+      <!-- 情况二:未做完上述操作 -->
+      <el-row v-else style="text-align: center" align="middle">
+        <!-- 选择素材 -->
+        <el-col :span="12" class="col-select">
+          <el-button type="success" @click="openMaterial">
+            素材库选择
+            <icon icon="ep:circle-check" />
+          </el-button>
+          <el-dialog title="选择图片" v-model="dialogImageVisible" width="90%" append-to-body>
+            <wx-material-select :obj-data="objDataRef" @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="beforeImageUpload"
+            :on-success="handleUploadSuccess"
+          >
+            <el-button type="primary">上传图片</el-button>
+            <template #tip>
+              <span>
+                <div class="el-upload__tip"
+                  >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div
+                ></span
+              >
+            </template>
+          </el-upload>
+        </el-col>
+      </el-row>
+    </el-tab-pane>
+    <!-- 类型 3:语音 -->
+    <el-tab-pane name="voice">
+      <template #label>
+        <el-row align="middle">
+          <icon icon="ep:phone" />
+          语音
+        </el-row>
+      </template>
 
-<!--<script>-->
-<!--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'-->
+      <div class="select-item2" v-if="objDataRef.url">
+        <p class="item-name">{{ objDataRef.name }}</p>
+        <div class="item-infos">
+          <wx-voice-player :url="objDataRef.url" />
+        </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 {-->
-<!--  name: 'WxReplySelect',-->
-<!--  components: {-->
-<!--    WxNews,-->
-<!--    WxMaterialSelect,-->
-<!--    WxVoicePlayer,-->
-<!--    WxVideoPlayer-->
-<!--  },-->
-<!--  props: {-->
-<!--    objData: {-->
-<!--      // 消息对象。-->
-<!--      type: Object, // 设置为 Object 的原因,方便属性的传递-->
-<!--      required: true-->
-<!--    },-->
-<!--    newsType: {-->
-<!--      // 图文类型:1、已发布图文;2、草稿箱图文-->
-<!--      type: String,-->
-<!--      default: '1'-->
-<!--    }-->
-<!--  },-->
-<!--  data() {-->
-<!--    return {-->
-<!--      tempPlayerObj: {-->
-<!--        type: '2'-->
-<!--      },-->
+import { getAccessToken } from '@/utils/auth'
+import { defineComponent } from 'vue'
 
-<!--      tempObj: new Map().set(-->
-<!--        // 临时缓存,切换消息类型的 tab 的时候,可以保存对应的数据;-->
-<!--        this.objData.type, // 消息类型-->
-<!--        Object.assign({}, this.objData)-->
-<!--      ), // 消息内容-->
+export default defineComponent({
+  components: {
+    WxNews,
+    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, // 图文-->
-<!--      dialogImageVisible: false, // 图片-->
-<!--      dialogVoiceVisible: false, // 语音-->
-<!--      dialogVideoVisible: false, // 视频-->
-<!--      dialogThumbVisible: false, // 缩略图-->
+      // 清空上传时的各种数据
+      fileList.value = []
+      uploadData.title = ''
+      uploadData.introduction = ''
 
-<!--      // ========== 文件上传(图片、语音、视频) ==========-->
-<!--      fileList: [], // 文件列表-->
-<!--      uploadData: {-->
-<!--        accountId: undefined,-->
-<!--        type: this.objData.type,-->
-<!--        title: '',-->
-<!--        introduction: ''-->
-<!--      },-->
-<!--      actionUrl: process.env.VUE_APP_BASE_API + '/admin-api/mp/material/upload-temporary',-->
-<!--      headers: { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部-->
-<!--    }-->
-<!--  },-->
-<!--  methods: {-->
-<!--    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) {-->
-<!--        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-->
-<!--      }-->
+      // 上传好的文件,本质是个素材,所以可以进行选中
+      let item = response.data
+      selectMaterial(item)
+    }
+    /**
+     * 切换消息类型的 tab
+     *
+     * @param tab tab  没用 暂时删了tab
+     */
+    const handleClick = () => {
+      // 设置后续文件上传的文件类型
+      uploadData.type = objDataRef.type
+      if (uploadData.type === 'music') {
+        // 【音乐】上传的是缩略图
+        uploadData.type = 'thumb'
+      }
 
-<!--      // 清空上传时的各种数据-->
-<!--      this.fileList = []-->
-<!--      this.uploadData.title = ''-->
-<!--      this.uploadData.introduction = ''-->
+      // 从 tempObj 临时缓存中,获取对应的数据,并设置回 objDataRef
+      let tempObjItem = tempObj.get(objDataRef.type)
+      if (tempObjItem) {
+        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()
 
-<!--      // 上传好的文件,本质是个素材,所以可以进行选中-->
-<!--      let item = response.data-->
-<!--      this.selectMaterial(item)-->
-<!--    },-->
-<!--    /**-->
-<!--     * 切换消息类型的 tab-->
-<!--     *-->
-<!--     * @param tab tab-->
-<!--     */-->
-<!--    handleClick(tab) {-->
-<!--      // 设置后续文件上传的文件类型-->
-<!--      this.uploadData.type = this.objData.type-->
-<!--      if (this.uploadData.type === 'music') {-->
-<!--        // 【音乐】上传的是缩略图-->
-<!--        this.uploadData.type = 'thumb'-->
-<!--      }-->
+      // 创建 tempObjItem 对象,并设置对应的值
+      let tempObjItem = {
+        type: '',
+        articles: '',
+        thumbMediaId: '',
+        thumbMediaUrl: '',
+        introduction: '',
+        title: '',
+        musicUrl: '',
+        hqMusicUrl: '',
+        mediaId: '',
+        url: '',
+        name: '',
+        description: ''
+      }
+      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-->
-<!--      let tempObjItem = this.tempObj.get(this.objData.type)-->
-<!--      if (tempObjItem) {-->
-<!--        this.objData.content = tempObjItem.content ? tempObjItem.content : null-->
-<!--        this.objData.mediaId = tempObjItem.mediaId ? tempObjItem.mediaId : null-->
-<!--        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()-->
+<style lang="scss" scoped>
+.public-account-management {
+  .el-input {
+    width: 70%;
+    margin-right: 2%;
+  }
+}
 
-<!--      // 创建 tempObjItem 对象,并设置对应的值-->
-<!--      let tempObjItem = {}-->
-<!--      tempObjItem.type = this.objData.type-->
-<!--      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>-->
+.pagination {
+  text-align: right;
+  margin-right: 25px;
+}
 
-<!--<style lang="scss" scoped>-->
-<!--.public-account-management {-->
-<!--  .el-input {-->
-<!--    width: 70%;-->
-<!--    margin-right: 2%;-->
-<!--  }-->
-<!--}-->
-<!--.pagination {-->
-<!--  text-align: right;-->
-<!--  margin-right: 25px;-->
-<!--}-->
-<!--.select-item {-->
-<!--  width: 280px;-->
-<!--  padding: 10px;-->
-<!--  margin: 0 auto 10px auto;-->
-<!--  border: 1px solid #eaeaea;-->
-<!--}-->
-<!--.select-item2 {-->
-<!--  padding: 10px;-->
-<!--  margin: 0 auto 10px auto;-->
-<!--  border: 1px solid #eaeaea;-->
-<!--}-->
-<!--.ope-row {-->
-<!--  padding-top: 10px;-->
-<!--  text-align: center;-->
-<!--}-->
-<!--.item-name {-->
-<!--  font-size: 12px;-->
-<!--  overflow: hidden;-->
-<!--  text-overflow: ellipsis;-->
-<!--  white-space: nowrap;-->
-<!--  text-align: center;-->
-<!--}-->
-<!--.el-form-item__content {-->
-<!--  line-height: unset !important;-->
-<!--}-->
-<!--.col-select {-->
-<!--  border: 1px solid rgb(234, 234, 234);-->
-<!--  padding: 50px 0px;-->
-<!--  height: 160px;-->
-<!--  width: 49.5%;-->
-<!--}-->
-<!--.col-select2 {-->
-<!--  border: 1px solid rgb(234, 234, 234);-->
-<!--  padding: 50px 0px;-->
-<!--  height: 160px;-->
-<!--}-->
-<!--.col-add {-->
-<!--  border: 1px solid rgb(234, 234, 234);-->
-<!--  padding: 50px 0px;-->
-<!--  height: 160px;-->
-<!--  width: 49.5%;-->
-<!--  float: right;-->
-<!--}-->
-<!--.avatar-uploader-icon {-->
-<!--  border: 1px solid #d9d9d9;-->
-<!--  font-size: 28px;-->
-<!--  color: #8c939d;-->
-<!--  width: 100px !important;-->
-<!--  height: 100px !important;-->
-<!--  line-height: 100px !important;-->
-<!--  text-align: center;-->
-<!--}-->
-<!--.material-img {-->
-<!--  width: 100%;-->
-<!--}-->
-<!--.thumb-div {-->
-<!--  display: inline-block;-->
-<!--  text-align: center;-->
-<!--}-->
-<!--.item-infos {-->
-<!--  width: 30%;-->
-<!--  margin: auto;-->
-<!--}-->
-<!--</style>-->
+.select-item {
+  width: 280px;
+  padding: 10px;
+  margin: 0 auto 10px auto;
+  border: 1px solid #eaeaea;
+}
+
+.select-item2 {
+  padding: 10px;
+  margin: 0 auto 10px auto;
+  border: 1px solid #eaeaea;
+}
+
+.ope-row {
+  padding-top: 10px;
+  text-align: center;
+}
+
+.input-margin-bottom {
+  margin-bottom: 2%;
+}
+
+.item-name {
+  font-size: 12px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: center;
+}
+
+.el-form-item__content {
+  line-height: unset !important;
+}
+
+.col-select {
+  border: 1px solid rgb(234, 234, 234);
+  padding: 50px 0px;
+  height: 160px;
+  width: 49.5%;
+}
+
+.col-select2 {
+  border: 1px solid rgb(234, 234, 234);
+  padding: 50px 0px;
+  height: 160px;
+}
+
+.col-add {
+  border: 1px solid rgb(234, 234, 234);
+  padding: 50px 0px;
+  height: 160px;
+  width: 49.5%;
+  float: right;
+}
+
+.avatar-uploader-icon {
+  border: 1px solid #d9d9d9;
+  font-size: 28px;
+  color: #8c939d;
+  width: 100px !important;
+  height: 100px !important;
+  line-height: 100px !important;
+  text-align: center;
+}
+
+.material-img {
+  width: 100%;
+}
+
+.thumb-div {
+  display: inline-block;
+  text-align: center;
+}
+
+.item-infos {
+  width: 30%;
+  margin: auto;
+}
+</style>

+ 58 - 90
src/views/mp/components/wx-video-play/main.vue

@@ -8,110 +8,78 @@
       存在的问题:mediaId 有效期是 3 天,超过时间后无法播放
     2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。
   ② 体验优化:弹窗关闭后,自动暂停视频的播放
+
 -->
 <template>
-  <div>
+  <div @click="playVideo()">
     <!-- 提示 -->
-    <div @click="playVideo()">
-      <el-icon>
-        <VideoPlay />
-      </el-icon>
+    <div>
+      <Icon icon="ep:video-play" class="mr-5px" />
       <p>点击播放视频</p>
     </div>
 
     <!-- 弹窗播放 -->
-    <el-dialog
-      title="视频播放"
-      v-model:visible="dialogVideo"
-      width="40%"
-      append-to-body
-      @close="closeDialog"
-    >
-      <video-player
-        v-if="playerOptions.sources[0].src"
-        class="video-player vjs-custom-skin"
-        ref="videoPlayerRef"
-        :playsinline="true"
-        :options="playerOptions"
-        @play="onPlayerPlay($event)"
-        @pause="onPlayerPause($event)"
-      />
+    <el-dialog v-model="dialogVideo" title="视频播放" width="40%" append-to-body>
+      <template #footer>
+        <video-player
+          v-if="dialogVideo"
+          class="video-player vjs-big-play-centered"
+          :src="url"
+          poster=""
+          crossorigin="anonymous"
+          playsinline
+          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>
   </div>
 </template>
 
-<script setup lang="ts" name="WxVideoPlayer">
-// 引入 videoPlayer 相关组件。教程:https://juejin.cn/post/6923056942281654285
-import { videoPlayer } from 'vue-video-player'
+<script lang="ts" name="WxVideoPlayer">
+//升级videojs6.0版本,重寫6.0版本
+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 'vue-video-player/src/custom-theme.css'
-import { VideoPlay } from '@element-plus/icons-vue'
 
-const props = defineProps({
-  url: {
-    // 视频地址,例如说:https://www.iocoder.cn/xxx.mp4
-    type: String,
-    required: true
-  }
-})
-const videoPlayerRef = ref()
-const dialogVideo = ref(false)
-const playerOptions = reactive({
-  playbackRates: [0.5, 1.0, 1.5, 2.0], // 播放速度
-  autoplay: false, // 如果 true,浏览器准备好时开始回放。
-  muted: false, // 默认情况下将会消除任何音频。
-  loop: false, // 导致视频一结束就重新开始。
-  preload: 'auto', // 建议浏览器在 <video> 加载元素后是否应该开始下载视频数据。auto 浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
-  language: 'zh-CN',
-  aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
-  fluid: true, // 当true时,Video.js player 将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
-  sources: [
-    {
-      type: 'video/mp4',
-      src: '' // 你的视频地址(必填)【重要】
+export default defineComponent({
+  components: {
+    VideoPlayer
+  },
+  props: {
+    url: {
+      // 视频地址,例如说:https://vjs.zencdn.net/v/oceans.mp4
+      type: String,
+      required: true
     }
-  ],
-  poster: '', // 你的封面地址
-  width: document.documentElement.clientWidth,
-  notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖 Video.js 无法播放媒体源时显示的默认信息。
-  controlBar: {
-    timeDivider: true,
-    durationDisplay: true,
-    remainingTimeDisplay: false,
-    fullscreenToggle: true //全屏按钮
-  }
-})
+  },
+  setup() {
+    // const videoPlayerRef = ref(null)
+    const dialogVideo = ref(false)
 
-const playVideo = () => {
-  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) {}
+    const handleEvent = (log) => {
+      console.log('Basic player event', log)
+    }
+    const playVideo = () => {
+      dialogVideo.value = true
+    }
 
-// methods: {
-//   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) {}
-// }
+    return { handleEvent, playVideo, dialogVideo }
+  }
+})
 </script>

+ 1 - 0
src/views/mp/components/wx-voice-play/main.vue

@@ -25,6 +25,7 @@
 
 <script setup lang="ts" name="WxVoicePlayer">
 // 因为微信语音是 amr 格式,所以需要用到 amr 解码器:https://www.npmjs.com/package/benz-amr-recorder
+
 import BenzAMRRecorder from 'benz-amr-recorder'
 
 const props = defineProps({

+ 16 - 5
src/views/mp/freePublish/index.vue

@@ -19,8 +19,14 @@
         </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 @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>
@@ -63,12 +69,19 @@
 import { getFreePublishPage, deleteFreePublish } from '@/api/mp/freePublish'
 import * as MpAccountApi from '@/api/mp/account'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
+
 const message = useMessage() // 消息弹窗
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 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, // 当前页数
   pageNo: 1, // 当前页数
   accountId: undefined // 当前页数
@@ -115,7 +128,6 @@ const resetQuery = () => {
   queryFormRef.value.resetFields()
   // 默认选中第一个
   if (accountList.value.length > 0) {
-    // @ts-ignore
     queryParams.accountId = accountList.value[0].id
   }
   handleQuery()
@@ -144,7 +156,6 @@ onMounted(async () => {
   accountList.value = await MpAccountApi.getSimpleAccountList()
   // 选中第一个
   if (accountList.value.length > 0) {
-    // @ts-ignore
     queryParams.accountId = accountList.value[0].id
   }
   await getList()

+ 37 - 16
src/views/mp/message/index.vue

@@ -51,8 +51,14 @@
         />
       </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="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>
   </ContentWrap>
@@ -85,16 +91,20 @@
             <el-tag type="danger">取消关注</el-tag>
           </div>
           <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 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 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 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 v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'">
             <el-tag>系统拍照发图</el-tag>
@@ -125,7 +135,8 @@
             <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
           </div>
           <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>
           </div>
           <div v-else-if="scope.row.type === 'location'">
@@ -175,23 +186,26 @@
     />
 
     <!-- 发送消息的弹窗 -->
-    <el-dialog title="粉丝消息列表" v-model:visible="open" width="50%">
-      <wx-msg :user-id="userId" v-if="open" />
+    <el-dialog title="粉丝消息列表" v-model="open" @click="openDialog()" width="50%">
+      <template #footer>
+        <wx-msg :user-id="userId" v-if="open" />
+      </template>
     </el-dialog>
   </ContentWrap>
 </template>
 <script setup lang="ts" name="MpMessage">
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
-// import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
+import WxVideoPlayer from '@/views/mp/components/wx-video-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 WxMusic from '@/views/mp/components/wx-music/main.vue'
-// import WxNews from '@/views/mp/components/wx-news/main.vue'
+import WxMusic from '@/views/mp/components/wx-music/main.vue'
+import WxNews from '@/views/mp/components/wx-news/main.vue'
 import * as MpAccountApi from '@/api/mp/account'
 import * as MpMessageApi from '@/api/mp/message'
+
 const message = useMessage() // 消息弹窗
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
@@ -210,7 +224,7 @@ const open = ref(false) // 是否显示弹出层
 const userId = ref(0) // 操作的用户编号
 const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   // 如果没有选中公众号账号,则进行提示。
   if (!queryParams.accountId) {
@@ -248,6 +262,13 @@ const handleSend = async (row) => {
   open.value = true
 }
 
+const openDialog = () => {
+  open.value = true
+}
+// const closeDiaLog = () => {
+//   open.value = false
+// }
+
 /** 初始化 **/
 onMounted(async () => {
   accountList.value = await MpAccountApi.getSimpleAccountList()

+ 363 - 1
src/views/mp/statistics/index.vue

@@ -1,3 +1,365 @@
 <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>
+
+<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>

+ 14 - 5
src/views/mp/tag/index.vue

@@ -28,13 +28,21 @@
         />
       </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="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" 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 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-form-item>
     </el-form>
@@ -91,6 +99,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import * as MpTagApi from '@/api/mp/tag'
 import * as MpAccountApi from '@/api/mp/account'
 import TagForm from './TagForm.vue'
+
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -106,7 +115,7 @@ const queryParams = reactive({
 const queryFormRef = ref() // 搜索的表单
 const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   // 如果没有选中公众号账号,则进行提示。
   if (!queryParams.accountId) {

+ 1 - 1
src/views/system/dict/data.vue

@@ -130,7 +130,7 @@ const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 const dicts = ref<DictTypeApi.DictTypeVO[]>() // 字典类型的列表
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/system/loginlog/index.vue

@@ -123,7 +123,7 @@ const queryParams = reactive({
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/system/menu/index.vue

@@ -126,7 +126,7 @@ const queryFormRef = ref() // 搜索的表单
 const isExpandAll = ref(false) // 是否展开,默认全部折叠
 const refreshTable = ref(true) // 重新渲染表格状态
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 64 - 0
src/views/system/notify/message/NotifyMessageDetail.vue

@@ -0,0 +1,64 @@
+<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>

+ 194 - 53
src/views/system/notify/message/index.vue

@@ -1,67 +1,208 @@
 <template>
-  <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:详情 -->
-        <XTextButton
-          preIcon="ep:view"
-          :title="t('action.detail')"
-          v-hasPermi="['system:notify-message:query']"
-          @click="handleDetail(row.id)"
+  <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"
         />
-      </template>
-    </XTable>
-  </ContentWrap>
-  <!-- 弹窗 -->
-  <XModal id="messageModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
-    <!-- 表单:详情 -->
-    <Descriptions
-      v-if="actionType === 'detail'"
-      :schema="allSchemas.detailSchema"
-      :data="detailData"
+      </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>
+
+  <!-- 列表 -->
+  <content-wrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="用户类型" align="center" prop="userType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="用户编号" align="center" prop="userId" width="80" />
+      <el-table-column label="模板编码" align="center" prop="templateCode" width="80" />
+      <el-table-column label="发送人名称" align="center" prop="templateNickname" width="180" />
+      <el-table-column
+        label="模版内容"
+        align="center"
+        prop="templateContent"
+        width="200"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        label="模版参数"
+        align="center"
+        prop="templateParams"
+        width="180"
+        show-overflow-tooltip
+      >
+        <template #default="scope"> {{ scope.row.templateParams }}</template>
+      </el-table-column>
+      <el-table-column label="模版类型" align="center" prop="templateType" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="scope.row.templateType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否已读" align="center" prop="readStatus" width="100">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.readStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="阅读时间"
+        align="center"
+        prop="readTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openDetail(scope.row)"
+            v-hasPermi="['system:notify-message:query']"
+          >
+            详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
     />
-    <template #footer>
-      <!-- 按钮:关闭 -->
-      <XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />
-    </template>
-  </XModal>
+  </content-wrap>
+
+  <!-- 表单弹窗:详情 -->
+  <NotifyMessageDetail ref="detailRef" />
 </template>
 <script setup lang="ts" name="NotifyMessage">
-// 业务相关的 import
-import { allSchemas } from './message.data'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
 import * as NotifyMessageApi from '@/api/system/notify/message'
+import NotifyMessageDetail from './NotifyMessageDetail.vue'
 
-const { t } = useI18n() // 国际化
-
-// 列表相关的变量
-const [registerTable] = useXTable({
-  allSchemas: allSchemas,
-  topActionSlots: false,
-  getListApi: NotifyMessageApi.getNotifyMessagePageApi
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userType: undefined,
+  userId: undefined,
+  templateCode: undefined,
+  templateType: undefined,
+  createTime: []
 })
+const queryFormRef = ref() // 搜索的表单
 
-// 弹窗相关的变量
-const modelVisible = ref(false) // 是否显示弹出层
-const modelTitle = ref('edit') // 弹出层标题
-const modelLoading = ref(false) // 弹出层loading
-const actionType = ref('') // 操作按钮的类型
-const actionLoading = ref(false) // 按钮 Loading
-const detailData = ref() // 详情 Ref
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await NotifyMessageApi.getNotifyMessagePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
 
-// 设置标题
-const setDialogTile = (type: string) => {
-  modelLoading.value = true
-  modelTitle.value = t('action.' + type)
-  actionType.value = type
-  modelVisible.value = true
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
 }
 
-// 详情操作
-const handleDetail = async (rowId: number) => {
-  setDialogTile('detail')
-  const res = await NotifyMessageApi.getNotifyMessageApi(rowId)
-  detailData.value = res
-  modelLoading.value = false
+/** 详情操作 */
+const detailRef = ref()
+const openDetail = (data: NotifyMessageApi.NotifyMessageVO) => {
+  detailRef.value.open(data)
 }
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
 </script>

+ 0 - 101
src/views/system/notify/message/message.data.ts

@@ -1,101 +0,0 @@
-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)

+ 46 - 0
src/views/system/notify/my/MyNotifyMessageDetail.vue

@@ -0,0 +1,46 @@
+<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>

+ 194 - 39
src/views/system/notify/my/index.vue

@@ -1,58 +1,213 @@
 <template>
-  <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <template #toolbar_buttons>
-        <!-- 操作:标记已读 -->
-        <XButton type="primary" preIcon="ep:zoom-in" title="标记已读" @click="handleUpdateList" />
-        <!-- 操作:全部已读 -->
-        <XButton type="primary" preIcon="ep:zoom-in" title="全部已读" @click="handleUpdateAll" />
-      </template>
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:已读 -->
-        <XTextButton
-          preIcon="ep:view"
-          title="已读"
-          v-hasPermi="['system:notify-message:query']"
-          v-if="!row.readStatus"
-          @click="handleUpdate([row.id])"
+  <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"
         />
-      </template>
-    </XTable>
-  </ContentWrap>
+      </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>
+    <!-- 列表 -->
+    <el-table
+      v-loading="loading"
+      :data="list"
+      ref="tableRef"
+      row-key="id"
+      @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>
+      </el-table-column>
+      <el-table-column
+        label="消息内容"
+        align="center"
+        prop="templateContent"
+        show-overflow-tooltip
+      />
+      <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>
+      </el-table-column>
+      <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>
+
 <script setup lang="ts" name="MyNotifyMessage">
-// 业务相关的 import
-import { allSchemas } from './my.data'
+import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
 import * as NotifyMessageApi from '@/api/system/notify/message'
-
+import MyNotifyMessageDetail from './MyNotifyMessageDetail.vue'
 const message = useMessage() // 消息
 
-// 列表相关的变量
-const [registerTable, { reload, getCheckboxRecords }] = useXTable({
-  allSchemas: allSchemas,
-  getListApi: NotifyMessageApi.getMyNotifyMessagePage
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+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[]
-  if (list.length === 0) {
-    return
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  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) => {
-  await NotifyMessageApi.updateNotifyMessageRead(ids)
-  message.success('标记已读成功!')
-  reload()
+/** 标记一条站内信已读 */
+const handleReadOne = async (id) => {
+  await NotifyMessageApi.updateNotifyMessageRead(id)
+  await getList()
 }
 
-// 标记全部已读
+/** 标记全部站内信已读 **/
 const handleUpdateAll = async () => {
   await NotifyMessageApi.updateAllNotifyMessageRead()
   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>

+ 0 - 58
src/views/system/notify/my/my.data.ts

@@ -1,58 +0,0 @@
-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)

+ 1 - 1
src/views/system/oauth2/token/index.vue

@@ -115,7 +115,7 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/system/operatelog/index.vue

@@ -156,7 +156,7 @@ const queryParams = reactive({
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/system/sensitiveWord/index.vue

@@ -168,7 +168,7 @@ const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 const tagList = ref([]) // 标签数组
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/system/sms/channel/index.vue

@@ -146,7 +146,7 @@ const queryParams = reactive({
   createTime: []
 })
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/system/tenant/index.vue

@@ -191,7 +191,7 @@ const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 const packageList = ref([]) //租户套餐列表
 
-/** 查询参数列表 */
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {

+ 1 - 1
src/views/system/tenantPackage/tenantPackage.data.ts

@@ -43,7 +43,7 @@ const crudSchemas = reactive<VxeCrudSchema>({
     {
       title: t('form.remark'),
       field: 'remark',
-      isTable: false,
+      isTable: true,
       isSearch: true,
       form: {
         component: 'Input',

+ 4 - 5
src/views/system/user/RoleForm.vue

@@ -8,12 +8,12 @@
         <el-input v-model="formData.nickname" :disabled="true" />
       </el-form-item>
       <el-form-item label="角色">
-        <el-select v-model="formData.roleIds" multiple placeholder="请选择">
+        <el-select v-model="formData.roleIds" multiple placeholder="请选择角色">
           <el-option
             v-for="item in roleOptions"
-            :key="parseInt(item.id)"
+            :key="item.id"
             :label="item.name"
-            :value="parseInt(item.id)"
+            :value="item.id"
           />
         </el-select>
       </el-form-item>
@@ -28,6 +28,7 @@
 </template>
 
 <script setup lang="ts">
+// TODO el-dialog 用 Dialog 组件
 import { assignUserRoleApi, PermissionAssignUserRoleReqVO } from '@/api/system/permission'
 
 interface Props {
@@ -86,5 +87,3 @@ const submit = async () => {
   }
 }
 </script>
-
-<style></style>

+ 12 - 6
src/views/system/user/index.vue

@@ -269,6 +269,7 @@
 import type { ElTree } from 'element-plus'
 import { handleTree, defaultProps } from '@/utils/tree'
 // 原vue3版本api方法都是Api结尾觉得见名知义,个人觉得这个可以形成规范
+// TODO 使用 DeptApi 这种形式哈
 import { getSimpleDeptList as getSimpleDeptListApi } from '@/api/system/dept'
 import { getSimplePostList as getSimplePostListApi, PostVO } from '@/api/system/post'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
@@ -279,16 +280,15 @@ import {
   updateUserStatusApi,
   UserVO
 } from '@/api/system/user'
-import { parseTime } from './utils'
-import AddForm from './AddForm.vue'
-import ImportForm from './ImportForm.vue'
-import RoleForm from './RoleForm.vue'
+import { parseTime } from './utils' // TODO 可以使用 formatTime 里的方法
+import AddForm from './AddForm.vue' // TODO 改成 UserForm
+import ImportForm from './ImportForm.vue' // TODO 改成 UserImportForm
+import RoleForm from './RoleForm.vue' // TODO 改成 UserAssignRoleForm
 import { getUserApi, getUserPageApi } from '@/api/system/user'
 import { getSimpleRoleList as getSimpleRoleListApi } from '@/api/system/role'
 import { listUserRolesApi } from '@/api/system/permission'
 import { CommonStatusEnum } from '@/utils/constants'
 import download from '@/utils/download'
-
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -304,10 +304,11 @@ const queryParams = reactive({
 const showSearch = ref(true)
 const showAddDialog = ref(false)
 
-// 数据字典-
+// 数据字典- // TODO 可以直接 vue 那 getIntDictOptions,这样一方面少个变量,也可以 getIntDictOptions
 const statusDictDatas = getDictOptions(DICT_TYPE.COMMON_STATUS)
 
 // ========== 创建部门树结构 ==========
+// TODO 要不把部门树拆成一个左侧的组件,然后点击后触发 handleDeptNodeClick
 const deptName = ref('')
 watch(
   () => deptName.value,
@@ -375,6 +376,7 @@ const resetQuery = () => {
 // 添加或编辑
 const addEditFormRef = ref()
 // 添加用户
+// TODO 可以参考别的模块哈,openForm;然后 tree 和 position 可以里面在加载下,让组件自己维护自己哈。
 const handleAdd = () => {
   addEditFormRef?.value.resetForm()
   // 获得下拉数据
@@ -389,6 +391,7 @@ const handleImport = () => {
 }
 
 // 用户导出
+// TODO 改成 await 的风格;
 const exportLoading = ref(false)
 const handleExport = () => {
   message
@@ -432,6 +435,7 @@ const handleCommand = (command: string, index: number, row: UserVO) => {
 }
 
 // 用户状态修改
+// TODO 改成 await 的风格;
 const handleStatusChange = (row: UserVO) => {
   let text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
   message
@@ -466,6 +470,7 @@ const handleUpdate = (row: UserVO) => {
 }
 
 // 删除用户
+// TODO 改成 await 的风格;
 const handleDelete = (row: UserVO) => {
   const ids = row.id
   message
@@ -481,6 +486,7 @@ const handleDelete = (row: UserVO) => {
 }
 
 // 重置密码
+// TODO 改成 await 的风格;
 const handleResetPwd = (row: UserVO) => {
   message.prompt('请输入"' + row.username + '"的新密码', t('common.reminder')).then(({ value }) => {
     resetUserPwdApi(row.id, value)