Bläddra i källkod

Merge remote-tracking branch 'yudao/dev' into contrib

dhb52 2 år sedan
förälder
incheckning
a00c612f36
76 ändrade filer med 2704 tillägg och 3680 borttagningar
  1. 15 0
      .env.front
  2. 16 0
      .vscode/launch.json
  3. 0 1
      README.md
  4. 3 2
      build/vite/index.ts
  5. 2 5
      build/vite/optimize.ts
  6. 1 2
      package.json
  7. 2 2
      src/api/pay/merchant/index.ts
  8. 5 0
      src/api/pay/order/index.ts
  9. 1 1
      src/components/Icon/src/IconSelect.vue
  10. 0 3
      src/components/XModal/index.ts
  11. 0 44
      src/components/XModal/src/XModal.vue
  12. 0 3
      src/components/XTable/index.ts
  13. 0 442
      src/components/XTable/src/XTable.vue
  14. 0 81
      src/components/XTable/src/style/dark.scss
  15. 0 6
      src/components/XTable/src/style/index.scss
  16. 0 16
      src/components/XTable/src/style/light.scss
  17. 0 26
      src/components/XTable/src/type.ts
  18. 14 11
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  19. 0 4
      src/components/index.ts
  20. 0 354
      src/hooks/web/useVxeCrudSchemas.ts
  21. 0 264
      src/hooks/web/useVxeGrid.ts
  22. 0 39
      src/hooks/web/useXTable.ts
  23. 0 5
      src/main.ts
  24. 0 223
      src/plugins/vxeTable/index.ts
  25. 0 20
      src/plugins/vxeTable/renderer/dataPicker.tsx
  26. 0 23
      src/plugins/vxeTable/renderer/dataTimeRangePicker.tsx
  27. 0 12
      src/plugins/vxeTable/renderer/dict.tsx
  28. 0 10
      src/plugins/vxeTable/renderer/html.tsx
  29. 0 20
      src/plugins/vxeTable/renderer/img.tsx
  30. 0 7
      src/plugins/vxeTable/renderer/index.tsx
  31. 0 15
      src/plugins/vxeTable/renderer/link.tsx
  32. 0 35
      src/plugins/vxeTable/renderer/preview.tsx
  33. 0 2
      src/types/auto-components.d.ts
  34. 0 1
      src/types/auto-imports.d.ts
  35. 4 0
      src/utils/constants.ts
  36. 2 1
      src/utils/dict.ts
  37. 3 3
      src/views/bpm/oa/leave/detail.vue
  38. 1 1
      src/views/infra/codegen/EditTable.vue
  39. 1 1
      src/views/infra/codegen/PreviewCode.vue
  40. 35 21
      src/views/infra/codegen/components/GenerateInfoForm.vue
  41. 176 172
      src/views/mp/autoReply/index.vue
  42. 36 0
      src/views/mp/components/WxMpSelect.vue
  43. 44 0
      src/views/mp/components/wx-account-select/main.vue
  44. 9 106
      src/views/mp/components/wx-editor/WxEditor.vue
  45. 1 1
      src/views/mp/components/wx-location/main.vue
  46. 91 109
      src/views/mp/components/wx-material-select/main.vue
  47. 181 178
      src/views/mp/components/wx-msg/main.vue
  48. 11 0
      src/views/mp/components/wx-msg/types.ts
  49. 224 275
      src/views/mp/draft/index.vue
  50. 21 50
      src/views/mp/freePublish/index.vue
  51. 152 166
      src/views/mp/material/index.vue
  52. 49 90
      src/views/mp/menu/index.vue
  53. 67 73
      src/views/mp/message/index.vue
  54. 12 7
      src/views/mp/tag/TagForm.vue
  55. 21 63
      src/views/mp/tag/index.vue
  56. 32 39
      src/views/mp/user/index.vue
  57. 142 0
      src/views/pay/app/AppForm.vue
  58. 0 71
      src/views/pay/app/app.data.ts
  59. 437 129
      src/views/pay/app/index.vue
  60. 115 0
      src/views/pay/order/OrderDetail.vue
  61. 319 62
      src/views/pay/order/index.vue
  62. 0 152
      src/views/pay/order/order.data.ts
  63. 115 0
      src/views/pay/refund/RefundDetail.vue
  64. 326 44
      src/views/pay/refund/index.vue
  65. 0 173
      src/views/pay/refund/refund.data.ts
  66. 1 1
      src/views/system/dept/index.vue
  67. 2 1
      src/views/system/dict/data/DictDataForm.vue
  68. 1 1
      src/views/system/dict/data/index.vue
  69. 1 1
      src/views/system/mail/account/account.data.ts
  70. 1 1
      src/views/system/mail/account/index.vue
  71. 1 1
      src/views/system/mail/log/index.vue
  72. 1 1
      src/views/system/mail/log/log.data.ts
  73. 1 1
      src/views/system/mail/template/index.vue
  74. 1 1
      src/views/system/mail/template/template.data.ts
  75. 4 4
      src/views/system/notify/template/index.vue
  76. 4 1
      src/views/system/post/PostForm.vue

+ 15 - 0
.env.front

@@ -17,3 +17,18 @@ VITE_API_URL=/admin-api
 
 # 打包路径
 VITE_BASE_PATH=/
+
+# 项目本地运行端口号, 与.vscode/launch.json配合
+VITE_PORT=80
+
+# 是否删除debugger
+VITE_DROP_DEBUGGER=false
+
+# 是否删除console.log
+VITE_DROP_CONSOLE=false
+
+# 是否sourcemap
+VITE_SOURCEMAP=true
+
+# 验证码的开关
+VITE_APP_CAPTCHA_ENABLE=false

+ 16 - 0
.vscode/launch.json

@@ -0,0 +1,16 @@
+{
+  // Use IntelliSense to learn about possible attributes.
+  // Hover to view descriptions of existing attributes.
+  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "type": "msedge",
+      "request": "launch",
+      "name": "Launch Edge against localhost",
+      "url": "http://localhost",
+      "webRoot": "${workspaceFolder}/src",
+      "sourceMaps": true
+    }
+  ]
+}

+ 0 - 1
README.md

@@ -42,7 +42,6 @@
 | [TypeScript](https://www.typescriptlang.org/docs/)                   | JavaScript 的超集   | 4.9.5  |
 | [pinia](https://pinia.vuejs.org/)                                    | Vue 存储库 替代 vuex5 | 2.0.33 |
 | [vueuse](https://vueuse.org/)                                        | 常用工具集            | 9.13.0 |
-| [vxe-table](https://vxetable.cn/)                                    | Vue 最强表单         | 4.3.10  |
 | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化              | 9.2.2  |
 | [vue-router](https://router.vuejs.org/)                              | Vue 路由           | 4.1.6  |
 | [windicss](https://cn.windicss.org/)                                 | 下一代工具优先的 CSS 框架  | 3.5.6  |

+ 3 - 2
build/vite/index.ts

@@ -13,7 +13,7 @@ import Components from 'unplugin-vue-components/vite'
 import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
 import viteCompression from 'vite-plugin-compression'
 import topLevelAwait from 'vite-plugin-top-level-await'
-import vueSetupExtend from 'vite-plugin-vue-setup-extend'
+import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus'
 import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
 import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
 
@@ -98,7 +98,8 @@ export function createVitePlugins() {
       deleteOriginFile: false //压缩后是否删除源文件
     }),
     ViteEjsPlugin(),
-    topLevelAwait({ // https://juejin.cn/post/7152191742513512485
+    topLevelAwait({
+      // https://juejin.cn/post/7152191742513512485
       // The export name of top-level await promise for each chunk module
       promiseExportName: '__tla',
       // The function to generate import names of top-level await promise in each chunk module

+ 2 - 5
build/vite/optimize.ts

@@ -18,10 +18,6 @@ const include = [
   'lodash-es',
   'nprogress',
   'animate.css',
-  'vxe-table',
-  'vxe-table/es/style',
-  'vxe-table/lib/locale/lang/zh-CN',
-  'vxe-table/lib/locale/lang/en-US',
   'web-storage-cache',
   '@iconify/iconify',
   '@vueuse/core',
@@ -79,7 +75,8 @@ const include = [
   'element-plus/es/components/dropdown-item/style/css',
   'element-plus/es/components/badge/style/css',
   'element-plus/es/components/breadcrumb/style/css',
-  'element-plus/es/components/breadcrumb-item/style/css'
+  'element-plus/es/components/breadcrumb-item/style/css',
+  'element-plus/es/components/image/style/css'
 ]
 
 const exclude = ['@iconify/json']

+ 1 - 2
package.json

@@ -68,7 +68,6 @@
     "vue-router": "^4.1.6",
     "vue-types": "^5.0.2",
     "vuedraggable": "^4.1.0",
-    "vxe-table": "^4.3.11",
     "web-storage-cache": "^1.1.1",
     "xe-utils": "^3.5.7",
     "xml-js": "^1.6.11"
@@ -126,7 +125,7 @@
     "vite-plugin-purge-icons": "^0.9.2",
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-top-level-await": "^1.3.0",
-    "vite-plugin-vue-setup-extend": "^0.4.0",
+    "vite-plugin-vue-setup-extend-plus": "^0.1.0",
     "vite-plugin-windicss": "^1.8.10",
     "vue-tsc": "^1.2.0",
     "windicss": "^3.5.6"

+ 2 - 2
src/api/pay/merchant/index.ts

@@ -39,9 +39,9 @@ export const getMerchant = (id: number) => {
 }
 
 // 根据商户名称搜索商户列表
-export const getMerchantListByName = (name: string) => {
+export const getMerchantListByName = (name?: string) => {
   return request.get({
-    url: '/pay/merchant/list-by-name?id=',
+    url: '/pay/merchant/list-by-name',
     params: {
       name: name
     }

+ 5 - 0
src/api/pay/order/index.ts

@@ -88,6 +88,11 @@ export const getOrder = async (id: number) => {
   return await request.get({ url: '/pay/order/get?id=' + id })
 }
 
+// 获得支付订单的明细
+export const getOrderDetail = async (id: number) => {
+  return await request.get({ url: '/pay/order/get-detail?id=' + id })
+}
+
 // 新增支付订单
 export const createOrder = async (data: OrderVO) => {
   return await request.post({ url: '/pay/order/create', data })

+ 1 - 1
src/components/Icon/src/IconSelect.vue

@@ -95,7 +95,7 @@ watch(
     return props.modelValue
   },
   () => {
-    if (props.modelValue) {
+    if (props.modelValue && props.modelValue.indexOf(':') >= 0) {
       currentActiveType.value = props.modelValue.substring(0, props.modelValue.indexOf(':') + 1)
       icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1)
     }

+ 0 - 3
src/components/XModal/index.ts

@@ -1,3 +0,0 @@
-import XModal from './src/XModal.vue'
-
-export { XModal }

+ 0 - 44
src/components/XModal/src/XModal.vue

@@ -1,44 +0,0 @@
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes'
-const slots = useSlots()
-
-const props = defineProps({
-  id: propTypes.string.def('model_1'),
-  modelValue: propTypes.bool.def(false),
-  fullscreen: propTypes.bool.def(false),
-  loading: propTypes.bool.def(false),
-  title: propTypes.string.def('弹窗'),
-  width: propTypes.string.def('40%'),
-  height: propTypes.string,
-  minWidth: propTypes.string.def('460'),
-  minHeight: propTypes.string.def('320'),
-  showFooter: propTypes.bool.def(true),
-  maskClosable: propTypes.bool.def(false),
-  escClosable: propTypes.bool.def(false)
-})
-
-const getBindValue = computed(() => {
-  const attrs = useAttrs()
-  const obj = { ...attrs, ...props }
-  return obj
-})
-</script>
-
-<template>
-  <vxe-modal v-bind="getBindValue" destroy-on-close show-zoom resize transfer>
-    <template v-if="slots.header" #header>
-      <slot name="header"></slot>
-    </template>
-    <ElScrollbar>
-      <template v-if="slots.default" #default>
-        <slot name="default"></slot>
-      </template>
-    </ElScrollbar>
-    <template v-if="slots.corner" #corner>
-      <slot name="corner"></slot>
-    </template>
-    <template v-if="slots.footer" #footer>
-      <slot name="footer"></slot>
-    </template>
-  </vxe-modal>
-</template>

+ 0 - 3
src/components/XTable/index.ts

@@ -1,3 +0,0 @@
-import XTable from './src/XTable.vue'
-
-export { XTable }

+ 0 - 442
src/components/XTable/src/XTable.vue

@@ -1,442 +0,0 @@
-<template>
-  <VxeGrid v-bind="getProps" ref="xGrid" :class="`${prefixCls}`" class="xtable-scrollbar">
-    <template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
-      <slot :name="item" v-bind="data || {}"></slot>
-    </template>
-  </VxeGrid>
-</template>
-<script setup lang="ts" name="XTable">
-import { PropType } from 'vue'
-import { SizeType, VxeGridInstance } from 'vxe-table'
-import { useAppStore } from '@/store/modules/app'
-import { useDesign } from '@/hooks/web/useDesign'
-import { XTableProps } from './type'
-import { isBoolean, isFunction } from '@/utils/is'
-import styleCss from './style/dark.scss?inline'
-import download from '@/utils/download'
-
-const { t } = useI18n()
-const message = useMessage() // 消息弹窗
-
-const appStore = useAppStore()
-
-const { getPrefixCls } = useDesign()
-const prefixCls = getPrefixCls('x-vxe-table')
-
-const attrs = useAttrs()
-const emit = defineEmits(['register'])
-const removeStyles = () => {
-  const filename = 'cssTheme'
-  //移除引入的文件名
-  const targetelement = 'style'
-  const targetattr = 'id'
-  const allsuspects = document.getElementsByTagName(targetelement)
-  for (let i = allsuspects.length; i >= 0; i--) {
-    if (
-      allsuspects[i] &&
-      allsuspects[i].getAttribute(targetattr) != null &&
-      allsuspects[i].getAttribute(targetattr)?.indexOf(filename) != -1
-    ) {
-      console.log(allsuspects[i], 'node')
-      allsuspects[i].parentNode?.removeChild(allsuspects[i])
-    }
-  }
-}
-const reImport = () => {
-  const head = document.getElementsByTagName('head')[0]
-  const style = document.createElement('style')
-  style.innerText = styleCss
-  style.id = 'cssTheme'
-  head.appendChild(style)
-}
-watch(
-  () => appStore.getIsDark,
-  () => {
-    if (appStore.getIsDark) {
-      reImport()
-    }
-    if (!appStore.getIsDark) {
-      removeStyles()
-    }
-  },
-  { immediate: true }
-)
-
-const currentSize = computed(() => {
-  let resSize: SizeType = 'small'
-  const appsize = appStore.getCurrentSize
-  switch (appsize) {
-    case 'large':
-      resSize = 'medium'
-      break
-    case 'default':
-      resSize = 'small'
-      break
-    case 'small':
-      resSize = 'mini'
-      break
-  }
-  return resSize
-})
-
-const props = defineProps({
-  options: {
-    type: Object as PropType<XTableProps>,
-    default: () => {}
-  }
-})
-const innerProps = ref<Partial<XTableProps>>()
-
-const getProps = computed(() => {
-  const options = innerProps.value || props.options
-  options.size = currentSize as any
-  options.height = 700
-  getOptionInitConfig(options)
-  getColumnsConfig(options)
-  getProxyConfig(options)
-  getPageConfig(options)
-  getToolBarConfig(options)
-  // console.log(options);
-  return {
-    ...options,
-    ...attrs
-  }
-})
-
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-
-let proxyForm = false
-
-const getOptionInitConfig = (options: XTableProps) => {
-  options.size = currentSize as any
-  options.rowConfig = {
-    isCurrent: true, // 当鼠标点击行时,是否要高亮当前行
-    isHover: true // 当鼠标移到行时,是否要高亮当前行
-  }
-}
-
-// columns
-const getColumnsConfig = (options: XTableProps) => {
-  const { allSchemas } = options
-  if (!allSchemas) return
-  if (allSchemas.printSchema) {
-    options.printConfig = {
-      columns: allSchemas.printSchema
-    }
-  }
-  if (allSchemas.formSchema) {
-    proxyForm = true
-    options.formConfig = {
-      enabled: true,
-      titleWidth: 110,
-      titleAlign: 'right',
-      items: allSchemas.searchSchema
-    }
-  }
-  if (allSchemas.tableSchema) {
-    options.columns = allSchemas.tableSchema
-  }
-}
-
-// 动态请求
-const getProxyConfig = (options: XTableProps) => {
-  const { getListApi, proxyConfig, data, isList } = options
-  if (proxyConfig || data) return
-  if (getListApi && isFunction(getListApi)) {
-    if (!isList) {
-      options.proxyConfig = {
-        seq: true, // 启用动态序号代理(分页之后索引自动计算为当前页的起始序号)
-        form: proxyForm, // 启用表单代理,当点击表单提交按钮时会自动触发 reload 行为
-        props: { result: 'list', total: 'total' },
-        ajax: {
-          query: async ({ page, form }) => {
-            let queryParams: any = Object.assign({}, JSON.parse(JSON.stringify(form)))
-            if (options.params) {
-              queryParams = Object.assign(queryParams, options.params)
-            }
-            if (!options?.treeConfig) {
-              queryParams.pageSize = page.pageSize
-              queryParams.pageNo = page.currentPage
-            }
-            return new Promise(async (resolve) => {
-              resolve(await getListApi(queryParams))
-            })
-          },
-          delete: ({ body }) => {
-            return new Promise(async (resolve) => {
-              if (options.deleteApi) {
-                resolve(await options.deleteApi(JSON.stringify(body)))
-              } else {
-                Promise.reject('未设置deleteApi')
-              }
-            })
-          },
-          queryAll: ({ form }) => {
-            const queryParams = Object.assign({}, JSON.parse(JSON.stringify(form)))
-            return new Promise(async (resolve) => {
-              if (options.getAllListApi) {
-                resolve(await options.getAllListApi(queryParams))
-              } else {
-                resolve(await getListApi(queryParams))
-              }
-            })
-          }
-        }
-      }
-    } else {
-      options.proxyConfig = {
-        seq: true, // 启用动态序号代理(分页之后索引自动计算为当前页的起始序号)
-        form: true, // 启用表单代理,当点击表单提交按钮时会自动触发 reload 行为
-        props: { result: 'data' },
-        ajax: {
-          query: ({ form }) => {
-            let queryParams: any = Object.assign({}, JSON.parse(JSON.stringify(form)))
-            if (options?.params) {
-              queryParams = Object.assign(queryParams, options.params)
-            }
-            return new Promise(async (resolve) => {
-              resolve(await getListApi(queryParams))
-            })
-          }
-        }
-      }
-    }
-  }
-  if (options.exportListApi) {
-    options.exportConfig = {
-      filename: options?.exportName,
-      // 默认选中类型
-      type: 'csv',
-      // 自定义数据量列表
-      modes: options?.getAllListApi ? ['current', 'all'] : ['current'],
-      columns: options?.allSchemas?.printSchema
-    }
-  }
-}
-
-// 分页
-const getPageConfig = (options: XTableProps) => {
-  const { pagination, pagerConfig, treeConfig, isList } = options
-  if (isList) return
-  if (treeConfig) {
-    options.treeConfig = options.treeConfig
-    return
-  }
-  if (pagerConfig) return
-  if (pagination) {
-    if (isBoolean(pagination)) {
-      options.pagerConfig = {
-        border: false, // 带边框
-        background: false, // 带背景颜色
-        perfect: false, // 配套的样式
-        pageSize: 10, // 每页大小
-        pagerCount: 7, // 显示页码按钮的数量
-        autoHidden: false, // 当只有一页时自动隐藏
-        pageSizes: [5, 10, 20, 30, 50, 100], // 每页大小选项列表
-        layouts: [
-          'PrevJump',
-          'PrevPage',
-          'JumpNumber',
-          'NextPage',
-          'NextJump',
-          'Sizes',
-          'FullJump',
-          'Total'
-        ]
-      }
-      return
-    }
-    options.pagerConfig = pagination
-  } else {
-    if (pagination != false) {
-      options.pagerConfig = {
-        border: false, // 带边框
-        background: false, // 带背景颜色
-        perfect: false, // 配套的样式
-        pageSize: 10, // 每页大小
-        pagerCount: 7, // 显示页码按钮的数量
-        autoHidden: false, // 当只有一页时自动隐藏
-        pageSizes: [5, 10, 20, 30, 50, 100], // 每页大小选项列表
-        layouts: [
-          'Sizes',
-          'PrevJump',
-          'PrevPage',
-          'Number',
-          'NextPage',
-          'NextJump',
-          'FullJump',
-          'Total'
-        ]
-      }
-    }
-  }
-}
-
-// tool bar
-const getToolBarConfig = (options: XTableProps) => {
-  const { toolBar, toolbarConfig, topActionSlots } = options
-  if (toolbarConfig) return
-  if (toolBar) {
-    if (!isBoolean(toolBar)) {
-      console.info(2)
-      options.toolbarConfig = toolBar
-      return
-    }
-  } else if (topActionSlots != false) {
-    options.toolbarConfig = {
-      slots: { buttons: 'toolbar_buttons' }
-    }
-  } else {
-    options.toolbarConfig = {
-      enabled: true
-    }
-  }
-}
-
-// 刷新列表
-const reload = () => {
-  const g = unref(xGrid)
-  if (!g) {
-    return
-  }
-  g.commitProxy('query')
-}
-
-// 删除
-const deleteData = async (id: string | number) => {
-  const g = unref(xGrid)
-  if (!g) {
-    return
-  }
-  const options = innerProps.value || props.options
-  if (!options.deleteApi) {
-    console.error('未传入delListApi')
-    return
-  }
-  return new Promise(async () => {
-    message.delConfirm().then(async () => {
-      await (options?.deleteApi && options?.deleteApi(id))
-      message.success(t('common.delSuccess'))
-      // 刷新列表
-      reload()
-    })
-  })
-}
-
-// 批量删除
-const deleteBatch = async () => {
-  const g = unref(xGrid)
-  if (!g) {
-    return
-  }
-  const rows = g.getCheckboxRecords() || g.getRadioRecord()
-  let ids: any[] = []
-  if (rows.length == 0) {
-    message.error('请选择数据')
-    return
-  } else {
-    rows.forEach((row) => {
-      ids.push(row.id)
-    })
-  }
-  const options = innerProps.value || props.options
-  if (options.deleteListApi) {
-    return new Promise(async () => {
-      message.delConfirm().then(async () => {
-        await (options?.deleteListApi && options?.deleteListApi(ids))
-        message.success(t('common.delSuccess'))
-        // 刷新列表
-        reload()
-      })
-    })
-  } else if (options.deleteApi) {
-    return new Promise(async () => {
-      message.delConfirm().then(async () => {
-        ids.forEach(async (id) => {
-          await (options?.deleteApi && options?.deleteApi(id))
-        })
-        message.success(t('common.delSuccess'))
-        // 刷新列表
-        reload()
-      })
-    })
-  } else {
-    console.error('未传入delListApi')
-    return
-  }
-}
-
-// 导出
-const exportList = async (fileName?: string) => {
-  const g = unref(xGrid)
-  if (!g) {
-    return
-  }
-  const options = innerProps.value || props.options
-  if (!options?.exportListApi) {
-    console.error('未传入exportListApi')
-    return
-  }
-  const queryParams = Object.assign({}, JSON.parse(JSON.stringify(g.getProxyInfo()?.form)))
-  message.exportConfirm().then(async () => {
-    const res = await (options?.exportListApi && options?.exportListApi(queryParams))
-    download.excel(res as unknown as Blob, fileName ? fileName : 'excel.xls')
-  })
-}
-
-// 获取查询参数
-const getSearchData = () => {
-  const g = unref(xGrid)
-  if (!g) {
-    return
-  }
-  const queryParams = Object.assign({}, JSON.parse(JSON.stringify(g.getProxyInfo()?.form)))
-  return queryParams
-}
-
-// 获取当前列
-const getCurrentColumn = () => {
-  const g = unref(xGrid)
-  if (!g) {
-    return
-  }
-  return g.getCurrentColumn()
-}
-
-// 获取当前选中列,redio
-const getRadioRecord = () => {
-  const g = unref(xGrid)
-  if (!g) {
-    return
-  }
-  return g.getRadioRecord(false)
-}
-
-// 获取当前选中列,checkbox
-const getCheckboxRecords = () => {
-  const g = unref(xGrid)
-  if (!g) {
-    return
-  }
-  return g.getCheckboxRecords(false)
-}
-const setProps = (prop: Partial<XTableProps>) => {
-  innerProps.value = { ...unref(innerProps), ...prop }
-}
-
-defineExpose({ reload, Ref: xGrid, getSearchData, deleteData, exportList })
-emit('register', {
-  reload,
-  getSearchData,
-  setProps,
-  deleteData,
-  deleteBatch,
-  exportList,
-  getCurrentColumn,
-  getRadioRecord,
-  getCheckboxRecords
-})
-</script>
-<style lang="scss">
-@import './style/index.scss';
-</style>

+ 0 - 81
src/components/XTable/src/style/dark.scss

@@ -1,81 +0,0 @@
-// 修改样式变量
-//@import 'vxe-table/styles/variable.scss';
-
-/*font*/
-$vxe-font-color: #e5e7eb;
-// $vxe-font-size: 14px !default;
-// $vxe-font-size-medium: 16px !default;
-// $vxe-font-size-small: 14px !default;
-// $vxe-font-size-mini: 12px !default;
-
-/*color*/
-$vxe-primary-color: #409eff !default;
-$vxe-success-color: #67c23a !default;
-$vxe-info-color: #909399 !default;
-$vxe-warning-color: #e6a23c !default;
-$vxe-danger-color: #f56c6c !default;
-$vxe-disabled-color: #bfbfbf !default;
-$vxe-primary-disabled-color: #c0c4cc !default;
-
-/*loading*/
-$vxe-loading-color: $vxe-primary-color !default;
-$vxe-loading-background-color: #1d1e1f !default;
-$vxe-loading-z-index: 999 !default;
-
-/*icon*/
-$vxe-icon-font-family: Verdana, Arial, Tahoma !default;
-$vxe-icon-background-color: #e5e7eb !default;
-
-/*toolbar*/
-$vxe-toolbar-background-color: #1d1e1f !default;
-$vxe-toolbar-button-border: #dcdfe6 !default;
-$vxe-toolbar-custom-active-background-color: #d9dadb !default;
-$vxe-toolbar-panel-background-color: #e5e7eb !default;
-
-$vxe-table-font-color: #e5e7eb;
-$vxe-table-header-background-color: #1d1e1f;
-$vxe-table-body-background-color: #141414;
-$vxe-table-row-striped-background-color: #1d1d1d;
-$vxe-table-row-hover-background-color: #1d1e1f;
-$vxe-table-row-hover-striped-background-color: #1e1e1e;
-$vxe-table-footer-background-color: #1d1e1f;
-$vxe-table-row-current-background-color: #302d2d;
-$vxe-table-column-current-background-color: #302d2d;
-$vxe-table-column-hover-background-color: #302d2d;
-$vxe-table-row-hover-current-background-color: #302d2d;
-$vxe-table-row-checkbox-checked-background-color: #3e3c37 !default;
-$vxe-table-row-hover-checkbox-checked-background-color: #615a4a !default;
-$vxe-table-menu-background-color: #1d1e1f;
-$vxe-table-border-width: 1px !default;
-$vxe-table-border-color: #4c4d4f !default;
-$vxe-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
-$vxe-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
-
-$vxe-form-background-color: #141414;
-
-/*pager*/
-$vxe-pager-background-color: #1d1e1f !default;
-$vxe-pager-perfect-background-color: #262727 !default;
-$vxe-pager-perfect-button-background-color: #a7a3a3 !default;
-
-$vxe-input-background-color: #141414;
-$vxe-input-border-color: #4c4d4f !default;
-
-$vxe-select-option-hover-background-color: #262626 !default;
-$vxe-select-panel-background-color: #141414 !default;
-$vxe-select-empty-color: #262626 !default;
-$vxe-optgroup-title-color: #909399 !default;
-
-/*button*/
-$vxe-button-default-background-color: #262626;
-$vxe-button-dropdown-panel-background-color: #141414;
-
-/*modal*/
-$vxe-modal-header-background-color: #141414;
-$vxe-modal-body-background-color: #141414;
-$vxe-modal-border-color: #3b3b3b;
-
-/*pulldown*/
-$vxe-pulldown-panel-background-color: #262626 !default;
-
-@import 'vxe-table/styles/index.scss';

+ 0 - 6
src/components/XTable/src/style/index.scss

@@ -1,6 +0,0 @@
-// @import 'vxe-table/styles/variable.scss';
-// @import 'vxe-table/styles/modules.scss';
-// @import './theme/light.scss';
-i {
-  border-color: initial;
-}

+ 0 - 16
src/components/XTable/src/style/light.scss

@@ -1,16 +0,0 @@
-// 修改样式变量
-// /*font*/
-// $vxe-font-size: 12px !default;
-// $vxe-font-size-medium: 16px !default;
-// $vxe-font-size-small: 14px !default;
-// $vxe-font-size-mini: 12px !default;
-/*color*/
-$vxe-primary-color: #409eff !default;
-$vxe-success-color: #67c23a !default;
-$vxe-info-color: #909399 !default;
-$vxe-warning-color: #e6a23c !default;
-$vxe-danger-color: #f56c6c !default;
-$vxe-disabled-color: #bfbfbf !default;
-$vxe-primary-disabled-color: #c0c4cc !default;
-
-@import 'vxe-table/styles/index.scss';

+ 0 - 26
src/components/XTable/src/type.ts

@@ -1,26 +0,0 @@
-import { CrudSchema } from '@/hooks/web/useCrudSchemas'
-import type { VxeGridProps, VxeGridPropTypes, VxeTablePropTypes } from 'vxe-table'
-
-export type XTableProps<D = any> = VxeGridProps<D> & {
-  allSchemas?: CrudSchema
-  height?: number // 高度 默认730
-  topActionSlots?: boolean // 是否开启表格内顶部操作栏插槽
-  treeConfig?: VxeTablePropTypes.TreeConfig // 树形表单配置
-  isList?: boolean // 是否不带分页的list
-  getListApi?: Function // 获取列表接口
-  getAllListApi?: Function // 获取全部数据接口 用于 vxe 导出
-  deleteApi?: Function // 删除接口
-  deleteListApi?: Function // 批量删除接口
-  exportListApi?: Function // 导出接口
-  exportName?: string // 导出文件夹名称
-  params?: any // 其他查询参数
-  pagination?: boolean | VxeGridPropTypes.PagerConfig // 分页配置参数
-  toolBar?: boolean | VxeGridPropTypes.ToolbarConfig // 右侧工具栏配置参数
-}
-export type XColumns = VxeGridPropTypes.Columns
-
-export type VxeTableColumn = {
-  field: string
-  title?: string
-  children?: VxeTableColumn[]
-} & Recordable

+ 14 - 11
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -188,7 +188,13 @@
       <!-- <div id="js-properties-panel" class="panel"></div> -->
       <!-- <div class="my-process-designer__canvas" ref="bpmn-canvas"></div> -->
     </div>
-    <XModal title="预览" width="80%" height="90%" v-model="previewModelVisible" destroy-on-close>
+    <Dialog
+      title="预览"
+      v-model="previewModelVisible"
+      width="80%"
+      :scroll="true"
+      max-height="600px"
+    >
       <!-- append-to-body -->
       <div v-highlight>
         <code class="hljs">
@@ -196,10 +202,7 @@
           {{ previewResult }}
         </code>
       </div>
-      <!-- <pre>
-        <code class="hljs" v-html="highlightedCode(previewType, previewResult)"></code>
-      </pre> -->
-    </XModal>
+    </Dialog>
   </div>
 </template>
 
@@ -231,7 +234,7 @@ import activitiModdleExtension from './plugins/extension-moddle/activiti'
 import flowableModdleExtension from './plugins/extension-moddle/flowable'
 // 引入json转换与高亮
 // import xml2js from 'xml-js'
-import xml2js from 'fast-xml-parser'
+// import xml2js from 'fast-xml-parser'
 import { XmlNode, XmlNodeType, parseXmlString } from 'steady-xml'
 // 代码高亮插件
 // import hljs from 'highlight.js/lib/highlight'
@@ -626,7 +629,7 @@ const elementsAlign = (align) => {
 const previewProcessXML = () => {
   console.log(bpmnModeler.saveXML, 'bpmnModeler')
   bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
-    console.log(xml, 'xml111111')
+    // console.log(xml, 'xml111111')
     previewResult.value = xml
     previewType.value = 'xml'
     previewModelVisible.value = true
@@ -634,7 +637,7 @@ const previewProcessXML = () => {
 }
 const previewProcessJson = () => {
   bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
-    console.log(xml, 'xml')
+    // console.log(xml, 'xml')
 
     // const rootNode = parseXmlString(xml)
     // console.log(rootNode, 'rootNoderootNode')
@@ -644,9 +647,9 @@ const previewProcessJson = () => {
     // console.log(JSON.stringify(rootNodes.parent.toJsObject()), 'rootNodes.toJSON()')
     // console.log(JSON.stringify(rootNodes.parent.toJSON()), 'rootNodes.toJSON()')
 
-    const parser = new xml2js.XMLParser()
-    let jObj = parser.parse(xml)
-    console.log(jObj, 'jObjjObjjObjjObjjObj')
+    // const parser = new xml2js.XMLParser()
+    // let jObj = parser.parse(xml)
+    // console.log(jObj, 'jObjjObjjObjjObjjObj')
     // const builder = new xml2js.XMLBuilder(xml)
     // const xmlContent = builder
     // console.log(xmlContent, 'xmlContent')

+ 0 - 4
src/components/index.ts

@@ -3,8 +3,6 @@ import { Icon } from './Icon'
 import { Form } from '@/components/Form'
 import { Table } from '@/components/Table'
 import { Search } from '@/components/Search'
-import { XModal } from '@/components/XModal'
-import { XTable } from '@/components/XTable'
 import { XButton, XTextButton } from '@/components/XButton'
 import { DictTag } from '@/components/DictTag'
 import { ContentWrap } from '@/components/ContentWrap'
@@ -15,8 +13,6 @@ export const setupGlobCom = (app: App<Element>): void => {
   app.component('Form', Form)
   app.component('Table', Table)
   app.component('Search', Search)
-  app.component('XModal', XModal)
-  app.component('XTable', XTable)
   app.component('XButton', XButton)
   app.component('XTextButton', XTextButton)
   app.component('DictTag', DictTag)

+ 0 - 354
src/hooks/web/useVxeCrudSchemas.ts

@@ -1,354 +0,0 @@
-import {
-  FormItemRenderOptions,
-  VxeColumnPropTypes,
-  VxeFormItemProps,
-  VxeGridPropTypes,
-  VxeTableDefines
-} from 'vxe-table'
-import { eachTree } from 'xe-utils'
-
-import { getBoolDictOptions, getDictOptions, getIntDictOptions } from '@/utils/dict'
-import { FormSchema } from '@/types/form'
-import { VxeTableColumn } from '@/types/table'
-import { ComponentOptions } from '@/types/components'
-import { DescriptionsSchema } from '@/types/descriptions'
-
-export type VxeCrudSchema = {
-  primaryKey?: string // 主键ID
-  primaryTitle?: string // 主键标题 默认为序号
-  primaryType?: VxeColumnPropTypes.Type | 'id' // 还支持 "id" | "seq" | "radio" | "checkbox" | "expand" | "html" | null
-  firstColumn?: VxeColumnPropTypes.Type // 第一列显示类型
-  action?: boolean // 是否开启表格内右侧操作栏插槽
-  actionTitle?: string // 操作栏标题 默认为操作
-  actionWidth?: string // 操作栏插槽宽度,一般2个字带图标 text 类型按钮 50-70
-  columns: VxeCrudColumns[]
-  searchSpan?: number
-}
-type VxeCrudColumns = Omit<VxeTableColumn, 'children'> & {
-  field: string // 字段名
-  title?: string // 标题名
-  formatter?: VxeColumnPropTypes.Formatter // vxe formatter格式化
-  isSearch?: boolean // 是否在查询显示
-  search?: CrudSearchParams // 查询的详细配置
-  isTable?: boolean // 是否在列表显示
-  table?: CrudTableParams // 列表的详细配置
-  isForm?: boolean // 是否在表单显示
-  form?: CrudFormParams // 表单的详细配置
-  isDetail?: boolean // 是否在详情显示
-  detail?: CrudDescriptionsParams // 详情的详细配置
-  print?: CrudPrintParams // vxe 打印的字段
-  children?: VxeCrudColumns[] // 子级
-  dictType?: string // 字典类型
-  dictClass?: 'string' | 'number' | 'boolean' // 字典数据类型 string | number | boolean
-}
-
-type CrudSearchParams = {
-  // 是否显示在查询项
-  show?: boolean
-} & Omit<VxeFormItemProps, 'field'>
-
-type CrudTableParams = {
-  // 是否显示表头
-  show?: boolean
-} & Omit<VxeTableDefines.ColumnOptions, 'field'>
-
-type CrudFormParams = {
-  // 是否显示表单项
-  show?: boolean
-} & Omit<FormSchema, 'field'>
-
-type CrudDescriptionsParams = {
-  // 是否显示表单项
-  show?: boolean
-} & Omit<DescriptionsSchema, 'field'>
-
-type CrudPrintParams = {
-  // 是否显示打印项
-  show?: boolean
-} & Omit<VxeTableDefines.ColumnInfo[], 'field'>
-
-export type VxeAllSchemas = {
-  searchSchema: VxeFormItemProps[]
-  tableSchema: VxeGridPropTypes.Columns
-  formSchema: FormSchema[]
-  detailSchema: DescriptionsSchema[]
-  printSchema: VxeTableDefines.ColumnInfo[]
-}
-
-// 过滤所有结构
-export const useVxeCrudSchemas = (
-  crudSchema: VxeCrudSchema
-): {
-  allSchemas: VxeAllSchemas
-} => {
-  // 所有结构数据
-  const allSchemas = reactive<VxeAllSchemas>({
-    searchSchema: [],
-    tableSchema: [],
-    formSchema: [],
-    detailSchema: [],
-    printSchema: []
-  })
-
-  const searchSchema = filterSearchSchema(crudSchema)
-  allSchemas.searchSchema = searchSchema || []
-
-  const tableSchema = filterTableSchema(crudSchema)
-  allSchemas.tableSchema = tableSchema || []
-
-  const formSchema = filterFormSchema(crudSchema)
-  allSchemas.formSchema = formSchema
-
-  const detailSchema = filterDescriptionsSchema(crudSchema)
-  allSchemas.detailSchema = detailSchema
-
-  const printSchema = filterPrintSchema(crudSchema)
-  allSchemas.printSchema = printSchema
-
-  return {
-    allSchemas
-  }
-}
-
-// 过滤 Search 结构
-const filterSearchSchema = (crudSchema: VxeCrudSchema): VxeFormItemProps[] => {
-  const { t } = useI18n()
-  const span = crudSchema.searchSpan ? crudSchema.searchSpan : 6
-  const spanLength = 24 / span
-  const searchSchema: VxeFormItemProps[] = []
-  eachTree(crudSchema.columns, (schemaItem: VxeCrudColumns) => {
-    // 判断是否显示
-    if (schemaItem?.isSearch || schemaItem.search?.show) {
-      let itemRenderName = schemaItem?.search?.itemRender?.name || '$input'
-      const options: any[] = []
-      let itemRender: FormItemRenderOptions = {}
-
-      if (schemaItem.dictType) {
-        const allOptions = { label: '全部', value: '' }
-        options.push(allOptions)
-        getDictOptions(schemaItem.dictType).forEach((dict) => {
-          options.push(dict)
-        })
-        itemRender.options = options
-        if (!schemaItem?.search?.itemRender?.name) itemRenderName = '$select'
-        itemRender = {
-          name: itemRenderName,
-          options: options,
-          props: { placeholder: t('common.selectText') }
-        }
-      } else {
-        if (schemaItem.search?.itemRender) {
-          itemRender = schemaItem.search.itemRender
-        } else {
-          itemRender = {
-            name: itemRenderName,
-            props:
-              itemRenderName == '$input'
-                ? { placeholder: t('common.inputText') }
-                : { placeholder: t('common.selectText') }
-          }
-        }
-      }
-      const searchSchemaItem = {
-        // 默认为 input
-        folding: searchSchema.length > spanLength - 1,
-        itemRender: schemaItem.itemRender ? schemaItem.itemRender : itemRender,
-        field: schemaItem.field,
-        title: schemaItem.search?.title || schemaItem.title,
-        slots: schemaItem.search?.slots,
-        span: span
-      }
-      searchSchema.push(searchSchemaItem)
-    }
-  })
-  if (searchSchema.length > 0) {
-    // 添加搜索按钮
-    const buttons: VxeFormItemProps = {
-      span: 24,
-      align: 'right',
-      collapseNode: searchSchema.length > spanLength,
-      itemRender: {
-        name: '$buttons',
-        children: [
-          { props: { type: 'submit', content: t('common.query'), status: 'primary' } },
-          { props: { type: 'reset', content: t('common.reset') } }
-        ]
-      }
-    }
-    searchSchema.push(buttons)
-  }
-  return searchSchema
-}
-
-// 过滤 table 结构
-const filterTableSchema = (crudSchema: VxeCrudSchema): VxeGridPropTypes.Columns => {
-  const { t } = useI18n()
-  const tableSchema: VxeGridPropTypes.Columns = []
-  // 第一列
-  if (crudSchema.firstColumn) {
-    const tableSchemaItem = {
-      type: crudSchema.firstColumn,
-      width: '50px'
-    }
-    tableSchema.push(tableSchemaItem)
-  }
-  // 主键ID
-  if (crudSchema.primaryKey && crudSchema.primaryType) {
-    const primaryTitle = crudSchema.primaryTitle ? crudSchema.primaryTitle : t('common.index')
-    const primaryWidth = primaryTitle.length * 30 + 'px'
-
-    let tableSchemaItem: { [x: string]: any } = {
-      title: primaryTitle,
-      field: crudSchema.primaryKey,
-      width: primaryWidth
-    }
-    if (crudSchema.primaryType != 'id') {
-      tableSchemaItem = {
-        ...tableSchemaItem,
-        type: crudSchema.primaryType
-      }
-    }
-    tableSchema.push(tableSchemaItem)
-  }
-
-  eachTree(crudSchema.columns, (schemaItem: VxeCrudColumns) => {
-    // 判断是否显示
-    if (schemaItem?.isTable !== false && schemaItem?.table?.show !== false) {
-      const tableSchemaItem = {
-        ...schemaItem.table,
-        field: schemaItem.field,
-        title: schemaItem.table?.title || schemaItem.title,
-        minWidth: '80px'
-      }
-      tableSchemaItem.showOverflow = 'tooltip'
-      if (schemaItem?.formatter) {
-        tableSchemaItem.formatter = schemaItem.formatter
-        tableSchemaItem.width = tableSchemaItem.width ? tableSchemaItem.width : 160
-      }
-      if (schemaItem?.dictType) {
-        tableSchemaItem.cellRender = {
-          name: 'XDict',
-          content: schemaItem.dictType
-        }
-        tableSchemaItem.width = tableSchemaItem.width ? tableSchemaItem.width : 160
-      }
-
-      tableSchema.push(tableSchemaItem)
-    }
-  })
-  // 操作栏插槽
-  if (crudSchema.action && crudSchema.action == true) {
-    const tableSchemaItem = {
-      title: crudSchema.actionTitle ? crudSchema.actionTitle : t('table.action'),
-      field: 'actionbtns',
-      fixed: 'right' as unknown as VxeColumnPropTypes.Fixed,
-      width: crudSchema.actionWidth ? crudSchema.actionWidth : '200px',
-      slots: {
-        default: 'actionbtns_default'
-      }
-    }
-    tableSchema.push(tableSchemaItem)
-  }
-  return tableSchema
-}
-
-// 过滤 form 结构
-const filterFormSchema = (crudSchema: VxeCrudSchema): FormSchema[] => {
-  const formSchema: FormSchema[] = []
-
-  eachTree(crudSchema.columns, (schemaItem: VxeCrudColumns) => {
-    // 判断是否显示
-    if (schemaItem?.isForm !== false && schemaItem?.form?.show !== false) {
-      // 默认为 input
-      let component = schemaItem?.form?.component || 'Input'
-      let defaultValue: any = ''
-      if (schemaItem.form?.value) {
-        defaultValue = schemaItem.form?.value
-      } else {
-        if (component === 'InputNumber') {
-          defaultValue = 0
-        }
-      }
-      let comonentProps = {}
-      if (schemaItem.dictType) {
-        const options: ComponentOptions[] = []
-        if (schemaItem.dictClass && schemaItem.dictClass === 'number') {
-          getIntDictOptions(schemaItem.dictType).forEach((dict) => {
-            options.push(dict)
-          })
-        } else if (schemaItem.dictClass && schemaItem.dictClass === 'boolean') {
-          getBoolDictOptions(schemaItem.dictType).forEach((dict) => {
-            options.push(dict)
-          })
-        } else {
-          getDictOptions(schemaItem.dictType).forEach((dict) => {
-            options.push(dict)
-          })
-        }
-        comonentProps = {
-          options: options
-        }
-        if (!(schemaItem.form && schemaItem.form.component)) component = 'Select'
-      }
-      const formSchemaItem = {
-        component: component,
-        componentProps: comonentProps,
-        value: defaultValue,
-        ...schemaItem.form,
-        field: schemaItem.field,
-        label: schemaItem.form?.label || schemaItem.title
-      }
-
-      formSchema.push(formSchemaItem)
-    }
-  })
-
-  return formSchema
-}
-
-// 过滤 descriptions 结构
-const filterDescriptionsSchema = (crudSchema: VxeCrudSchema): DescriptionsSchema[] => {
-  const descriptionsSchema: DescriptionsSchema[] = []
-
-  eachTree(crudSchema.columns, (schemaItem: VxeCrudColumns) => {
-    // 判断是否显示
-    if (schemaItem?.isDetail !== false && schemaItem.detail?.show !== false) {
-      const descriptionsSchemaItem = {
-        ...schemaItem.detail,
-        field: schemaItem.field,
-        label: schemaItem.detail?.label || schemaItem.title
-      }
-      if (schemaItem.dictType) {
-        descriptionsSchemaItem.dictType = schemaItem.dictType
-      }
-      if (schemaItem.detail?.dateFormat || schemaItem.formatter == 'formatDate') {
-        // 优先使用 detail 下的配置,如果没有默认为 YYYY-MM-DD HH:mm:ss
-        descriptionsSchemaItem.dateFormat = schemaItem?.detail?.dateFormat
-          ? schemaItem?.detail?.dateFormat
-          : 'YYYY-MM-DD HH:mm:ss'
-      }
-
-      descriptionsSchema.push(descriptionsSchemaItem)
-    }
-  })
-
-  return descriptionsSchema
-}
-
-// 过滤 打印 结构
-const filterPrintSchema = (crudSchema: VxeCrudSchema): any[] => {
-  const printSchema: any[] = []
-
-  eachTree(crudSchema.columns, (schemaItem: VxeCrudColumns) => {
-    // 判断是否显示
-    if (schemaItem?.print?.show !== false) {
-      const printSchemaItem = {
-        field: schemaItem.field
-      }
-
-      printSchema.push(printSchemaItem)
-    }
-  })
-
-  return printSchema
-}

+ 0 - 264
src/hooks/web/useVxeGrid.ts

@@ -1,264 +0,0 @@
-import { computed, nextTick, reactive } from 'vue'
-import { SizeType, VxeGridProps, VxeTablePropTypes } from 'vxe-table'
-import { useAppStore } from '@/store/modules/app'
-import { VxeAllSchemas } from './useVxeCrudSchemas'
-
-import download from '@/utils/download'
-
-const { t } = useI18n()
-const message = useMessage() // 消息弹窗
-
-interface UseVxeGridConfig<T = any> {
-  allSchemas: VxeAllSchemas
-  height?: number // 高度 默认730
-  topActionSlots?: boolean // 是否开启表格内顶部操作栏插槽
-  treeConfig?: VxeTablePropTypes.TreeConfig // 树形表单配置
-  isList?: boolean // 是否不带分页的list
-  getListApi: (option: any) => Promise<T> // 获取列表接口
-  getAllListApi?: (option: any) => Promise<T> // 获取全部数据接口 用于VXE导出
-  deleteApi?: (option: any) => Promise<T> // 删除接口
-  exportListApi?: (option: any) => Promise<T> // 导出接口
-  exportName?: string // 导出文件夹名称
-  queryParams?: any // 其他查询参数
-}
-
-const appStore = useAppStore()
-
-const currentSize = computed(() => {
-  let resSize: SizeType = 'small'
-  const appsize = appStore.getCurrentSize
-  switch (appsize) {
-    case 'large':
-      resSize = 'medium'
-      break
-    case 'default':
-      resSize = 'small'
-      break
-    case 'small':
-      resSize = 'mini'
-      break
-  }
-  return resSize
-})
-
-export const useVxeGrid = <T = any>(config?: UseVxeGridConfig<T>) => {
-  /**
-   * grid options 初始化
-   */
-  const gridOptions = reactive<VxeGridProps<any>>({
-    loading: true,
-    size: currentSize as any,
-    height: config?.height ? config.height : 730,
-    rowConfig: {
-      isCurrent: true, // 当鼠标点击行时,是否要高亮当前行
-      isHover: true // 当鼠标移到行时,是否要高亮当前行
-    },
-    toolbarConfig: {
-      slots:
-        !config?.topActionSlots && config?.topActionSlots != false
-          ? { buttons: 'toolbar_buttons' }
-          : {}
-    },
-    printConfig: {
-      columns: config?.allSchemas.printSchema
-    },
-    formConfig: {
-      enabled: true,
-      titleWidth: 100,
-      titleAlign: 'right',
-      items: config?.allSchemas.searchSchema
-    },
-    columns: config?.allSchemas.tableSchema,
-    proxyConfig: {
-      seq: true, // 启用动态序号代理(分页之后索引自动计算为当前页的起始序号)
-      form: true, // 启用表单代理,当点击表单提交按钮时会自动触发 reload 行为
-      props: { result: 'list', total: 'total' },
-      ajax: {
-        query: ({ page, form }) => {
-          let queryParams: any = Object.assign({}, JSON.parse(JSON.stringify(form)))
-          if (config?.queryParams) {
-            queryParams = Object.assign(queryParams, config.queryParams)
-          }
-          if (!config?.treeConfig) {
-            queryParams.pageSize = page.pageSize
-            queryParams.pageNo = page.currentPage
-          }
-          gridOptions.loading = false
-          return new Promise(async (resolve) => {
-            resolve(await config?.getListApi(queryParams))
-          })
-        },
-        delete: ({ body }) => {
-          return new Promise(async (resolve) => {
-            if (config?.deleteApi) {
-              resolve(await config?.deleteApi(JSON.stringify(body)))
-            } else {
-              Promise.reject('未设置deleteApi')
-            }
-          })
-        },
-        queryAll: ({ form }) => {
-          const queryParams = Object.assign({}, JSON.parse(JSON.stringify(form)))
-          return new Promise(async (resolve) => {
-            if (config?.getAllListApi) {
-              resolve(await config?.getAllListApi(queryParams))
-            } else {
-              resolve(await config?.getListApi(queryParams))
-            }
-          })
-        }
-      }
-    },
-    exportConfig: {
-      filename: config?.exportName,
-      // 默认选中类型
-      type: 'csv',
-      // 自定义数据量列表
-      modes: config?.getAllListApi ? ['current', 'all'] : ['current'],
-      columns: config?.allSchemas.printSchema
-    }
-  })
-
-  if (config?.treeConfig) {
-    gridOptions.treeConfig = config.treeConfig
-  } else if (config?.isList) {
-    gridOptions.proxyConfig = {
-      seq: true, // 启用动态序号代理(分页之后索引自动计算为当前页的起始序号)
-      form: true, // 启用表单代理,当点击表单提交按钮时会自动触发 reload 行为
-      props: { result: 'data' },
-      ajax: {
-        query: ({ form }) => {
-          let queryParams: any = Object.assign({}, JSON.parse(JSON.stringify(form)))
-          if (config?.queryParams) {
-            queryParams = Object.assign(queryParams, config.queryParams)
-          }
-          gridOptions.loading = false
-          return new Promise(async (resolve) => {
-            resolve(await config?.getListApi(queryParams))
-          })
-        }
-      }
-    }
-  } else {
-    gridOptions.pagerConfig = {
-      border: false, // 带边框
-      background: true, // 带背景颜色
-      perfect: false, // 配套的样式
-      pageSize: 10, // 每页大小
-      pagerCount: 7, // 显示页码按钮的数量
-      autoHidden: false, // 当只有一页时自动隐藏
-      pageSizes: [5, 10, 20, 30, 50, 100], // 每页大小选项列表
-      layouts: [
-        'PrevJump',
-        'PrevPage',
-        'JumpNumber',
-        'NextPage',
-        'NextJump',
-        'Sizes',
-        'FullJump',
-        'Total'
-      ]
-    }
-  }
-
-  /**
-   * 刷新列表
-   * @param ref
-   * @returns
-   */
-  const getList = async (ref) => {
-    if (!ref) {
-      console.error('未传入gridRef')
-      return
-    }
-    await nextTick()
-    ref.value.commitProxy('query')
-  }
-
-  // 获取查询参数
-  const getSearchData = async (ref) => {
-    if (!ref) {
-      console.error('未传入gridRef')
-      return
-    }
-    await nextTick()
-    const queryParams = Object.assign(
-      {},
-      JSON.parse(JSON.stringify(ref.value.getProxyInfo()?.form))
-    )
-    return queryParams
-  }
-
-  /**
-   * 删除
-   * @param ref
-   * @param ids rowid
-   * @returns
-   */
-  const deleteData = async (ref, ids: string | number) => {
-    if (!ref) {
-      console.error('未传入gridRef')
-      return
-    }
-    if (!config?.deleteApi) {
-      console.error('未传入delListApi')
-      return
-    }
-    await nextTick()
-    return new Promise(async () => {
-      message.delConfirm().then(async () => {
-        await (config?.deleteApi && config?.deleteApi(ids))
-        message.success(t('common.delSuccess'))
-        // 刷新列表
-        ref.value.commitProxy('query')
-      })
-    })
-  }
-  /**
-   * 导出
-   * @param ref
-   * @param fileName 文件名,默认excel.xls
-   * @returns
-   */
-  const exportList = async (ref, fileName?: string) => {
-    if (!ref) {
-      console.error('未传入gridRef')
-      return
-    }
-    if (!config?.exportListApi) {
-      console.error('未传入exportListApi')
-      return
-    }
-    await nextTick()
-    const queryParams = Object.assign(
-      {},
-      JSON.parse(JSON.stringify(ref.value?.getProxyInfo()?.form))
-    )
-    message.exportConfirm().then(async () => {
-      const res = await (config?.exportListApi && config?.exportListApi(queryParams))
-      download.excel(res as unknown as Blob, fileName ? fileName : 'excel.xls')
-    })
-  }
-  /**
-   * 表格最大/最小化
-   * @param ref
-   * @returns
-   */
-  const zoom = async (ref) => {
-    if (!ref) {
-      console.error('未传入gridRef')
-      return
-    }
-    await nextTick()
-    ref.value.zoom(!ref.value.isMaximized())
-  }
-
-  return {
-    gridOptions,
-    getList,
-    getSearchData,
-    deleteData,
-    exportList,
-    zoom
-  }
-}

+ 0 - 39
src/hooks/web/useXTable.ts

@@ -1,39 +0,0 @@
-import { XTableProps } from '@/components/XTable/src/type'
-
-export interface tableMethod {
-  reload: () => void // 刷新表格
-  setProps: (props: XTableProps) => void
-  deleteData: (id: string | number) => void // 删除数据
-  deleteBatch: () => void // 批量删除
-  exportList: (fileName?: string) => void // 导出列表
-  getCurrentColumn: () => void // 获取当前列
-  getRadioRecord: () => void // 获取当前选中列,radio
-  getCheckboxRecords: () => void //获取当前选中列, checkbox
-}
-
-export const useXTable = (props: XTableProps): [Function, tableMethod] => {
-  const tableRef = ref<Nullable<tableMethod>>(null)
-
-  const register = (instance) => {
-    tableRef.value = instance
-    props && instance.setProps(props)
-  }
-  const getInstance = (): tableMethod => {
-    const table = unref(tableRef)
-    if (!table) {
-      console.error('表格实例不存在')
-    }
-    return table as tableMethod
-  }
-  const methods: tableMethod = {
-    reload: () => getInstance().reload(),
-    setProps: (props) => getInstance().setProps(props),
-    deleteData: (id: string | number) => getInstance().deleteData(id),
-    deleteBatch: () => getInstance().deleteBatch(),
-    exportList: (fileName?: string) => getInstance().exportList(fileName),
-    getCurrentColumn: () => getInstance().getCheckboxRecords(),
-    getRadioRecord: () => getInstance().getRadioRecord(),
-    getCheckboxRecords: () => getInstance().getCheckboxRecords()
-  }
-  return [register, methods]
-}

+ 0 - 5
src/main.ts

@@ -16,9 +16,6 @@ import { setupGlobCom } from '@/components'
 // 引入 element-plus
 import { setupElementPlus } from '@/plugins/elementPlus'
 
-// 引入 vxe-table
-import { setupVxeTable } from '@/plugins/vxeTable'
-
 // 引入 form-create
 import { setupFormCreate } from '@/plugins/formCreate'
 
@@ -83,8 +80,6 @@ const setupAll = async () => {
 
   setupElementPlus(app)
 
-  setupVxeTable(app)
-
   setupFormCreate(app)
 
   setupRouter(app)

+ 0 - 223
src/plugins/vxeTable/index.ts

@@ -1,223 +0,0 @@
-import { App } from 'vue'
-import XEUtils from 'xe-utils'
-import './renderer'
-import 'vxe-table/lib/style.css'
-import { i18n } from '@/plugins/vueI18n'
-import zhCN from 'vxe-table/lib/locale/lang/zh-CN'
-import enUS from 'vxe-table/lib/locale/lang/en-US'
-import {
-  // 全局对象
-  VXETable,
-  // 表格功能
-  Filter,
-  Edit,
-  Menu,
-  Export,
-  Keyboard,
-  Validator,
-  // 可选组件
-  Icon,
-  Column,
-  Colgroup,
-  Grid,
-  Tooltip,
-  Toolbar,
-  Pager,
-  Form,
-  FormItem,
-  FormGather,
-  Checkbox,
-  CheckboxGroup,
-  Radio,
-  RadioGroup,
-  RadioButton,
-  Switch,
-  Input,
-  Select,
-  Optgroup,
-  Option,
-  Textarea,
-  Button,
-  Modal,
-  List,
-  Pulldown,
-  // 表格
-  Table
-} from 'vxe-table'
-
-// 全局默认参数
-VXETable.setup({
-  size: 'medium', // 全局尺寸
-  version: 0, // 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据
-  zIndex: 1008, // 全局 zIndex 起始值,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡
-  loadingText: '加载中...', // 全局loading提示内容,如果为null则不显示文本
-  height: 600,
-  table: {
-    border: 'inner', // default(默认), full(完整边框), outer(外边框), inner(内边框), none(无边框)
-    align: 'center', // eft(左对齐), center(居中对齐), right(右对齐)
-    autoResize: true, // 自动监听父元素的变化去重新计算表格
-    resizable: true, // 列是否允许拖动列宽调整大小
-    emptyText: '暂无数据', // 空表单
-    highlightHoverRow: true, // 自动监听父元素的变化去重新计算表格
-    treeConfig: {
-      rowField: 'id',
-      parentField: 'parentId',
-      children: 'children',
-      indent: 20,
-      showIcon: true
-    }
-  },
-  grid: {
-    toolbarConfig: {
-      refresh: true,
-      export: true,
-      print: true,
-      zoom: true,
-      custom: true
-    },
-    pagerConfig: {
-      border: false,
-      background: false,
-      autoHidden: true,
-      perfect: true,
-      pageSize: 10,
-      pagerCount: 7,
-      pageSizes: [5, 10, 15, 20, 50, 100, 200, 500],
-      layouts: [
-        'Sizes',
-        'PrevJump',
-        'PrevPage',
-        'Number',
-        'NextPage',
-        'NextJump',
-        'FullJump',
-        'Total'
-      ]
-    }
-  },
-  pager: {
-    background: false,
-    autoHidden: false,
-    perfect: true,
-    pageSize: 10,
-    pagerCount: 7,
-    pageSizes: [10, 15, 20, 50, 100],
-    layouts: ['PrevJump', 'PrevPage', 'Jump', 'PageCount', 'NextPage', 'NextJump', 'Sizes', 'Total']
-  },
-  input: {
-    clearable: true
-  },
-  form: {
-    titleColon: true // 是否显示标题冒号
-  },
-  modal: {
-    width: 800, // 窗口的宽度
-    height: 600, // 窗口的高度
-    minWidth: 460,
-    minHeight: 320,
-    showZoom: true, // 标题是否标显示最大化与还原按钮
-    resize: true, // 是否允许窗口边缘拖动调整窗口大小
-    marginSize: 0, // 只对 resize 启用后有效,用于设置可拖动界限范围,如果为负数则允许拖动超出屏幕边界
-    remember: false, // 记忆功能,会记住最后操作状态,再次打开窗口时还原窗口状态
-    destroyOnClose: true, // 在窗口关闭时销毁内容
-    storage: false, // 是否启用 localStorage 本地保存,会将窗口拖动的状态保存到本地
-    transfer: true, // 是否将弹框容器插入于 body 内
-    showFooter: true, // 是否显示底部
-    mask: true, // 是否显示遮罩层
-    maskClosable: true, // 是否允许点击遮罩层关闭窗口
-    escClosable: true // 是否允许按 Esc 键关闭窗口
-  },
-  i18n: (key, args) => {
-    return unref(i18n.global.locale) === 'zh-CN'
-      ? XEUtils.toFormatString(XEUtils.get(zhCN, key), args)
-      : XEUtils.toFormatString(XEUtils.get(enUS, key), args)
-  }
-})
-// 自定义全局的格式化处理函数
-VXETable.formats.mixin({
-  // 格式精简日期,默认 yyyy-MM-dd HH:mm:ss
-  formatDay({ cellValue }, format) {
-    if (cellValue != null) {
-      return XEUtils.toDateString(cellValue, format || 'yyyy-MM-dd')
-    } else {
-      return ''
-    }
-  },
-  // 格式完整日期,默认 yyyy-MM-dd HH:mm:ss
-  formatDate({ cellValue }, format) {
-    if (cellValue != null) {
-      return XEUtils.toDateString(cellValue, format || 'yyyy-MM-dd HH:mm:ss')
-    } else {
-      return ''
-    }
-  },
-  // 四舍五入金额,每隔3位逗号分隔,默认2位数
-  formatAmount({ cellValue }, digits = 2) {
-    return XEUtils.commafy(Number(cellValue), { digits })
-  },
-  // 格式化银行卡,默认每4位空格隔开
-  formatBankcard({ cellValue }) {
-    return XEUtils.commafy(XEUtils.toValueString(cellValue), { spaceNumber: 4, separator: ' ' })
-  },
-  // 四舍五入,默认两位数
-  formatFixedNumber({ cellValue }, digits = 2) {
-    return XEUtils.toFixed(XEUtils.round(cellValue, digits), digits)
-  },
-  // 向下舍入,默认两位数
-  formatCutNumber({ cellValue }, digits = 2) {
-    return XEUtils.toFixed(XEUtils.floor(cellValue, digits), digits)
-  },
-  // 格式化图片,将图片链接转换为html标签
-  formatImg({ cellValue }) {
-    return '<img height="40" src="' + cellValue + '"> '
-  },
-  // 格式化文件大小
-  formatSize({ cellValue }, digits = 0) {
-    const unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
-    const srcSize = parseFloat(cellValue)
-    const index = Math.floor(Math.log(srcSize) / Math.log(1024))
-    const size = srcSize / Math.pow(1024, index)
-    return XEUtils.toFixed(XEUtils.floor(size, 2), 2) + ' ' + unitArr[digits]
-  }
-})
-export const setupVxeTable = (app: App<Element>) => {
-  // 表格功能
-  app.use(Filter).use(Edit).use(Menu).use(Export).use(Keyboard).use(Validator)
-
-  // 可选组件
-  app
-    .use(Icon)
-    .use(Column)
-    .use(Colgroup)
-    .use(Grid)
-    .use(Tooltip)
-    .use(Toolbar)
-    .use(Pager)
-    .use(Form)
-    .use(FormItem)
-    .use(FormGather)
-    .use(Checkbox)
-    .use(CheckboxGroup)
-    .use(Radio)
-    .use(RadioGroup)
-    .use(RadioButton)
-    .use(Switch)
-    .use(Input)
-    .use(Select)
-    .use(Optgroup)
-    .use(Option)
-    .use(Textarea)
-    .use(Button)
-    .use(Modal)
-    .use(List)
-    .use(Pulldown)
-
-    // 安装表格
-    .use(Table)
-
-  // 给 vue 实例挂载内部对象,例如:
-  // app.config.globalProperties.$XModal = VXETable.modal
-  // app.config.globalProperties.$XPrint = VXETable.print
-  // app.config.globalProperties.$XSaveFile = VXETable.saveFile
-  // app.config.globalProperties.$XReadFile = VXETable.readFile
-}

+ 0 - 20
src/plugins/vxeTable/renderer/dataPicker.tsx

@@ -1,20 +0,0 @@
-import { ElDatePicker } from 'element-plus'
-import { VXETable } from 'vxe-table'
-
-// 日期区间选择渲染
-VXETable.renderer.add('XDataPicker', {
-  // 默认显示模板
-  renderItemContent(renderOpts, params) {
-    const { data, field } = params
-    const { content } = renderOpts
-    return (
-      <ElDatePicker
-        v-model={data[field]}
-        style="width: 100%"
-        type={content ? (content as any) : 'datetime'}
-        value-format="YYYY-MM-DD HH:mm:ss"
-        clearable
-      ></ElDatePicker>
-    )
-  }
-})

+ 0 - 23
src/plugins/vxeTable/renderer/dataTimeRangePicker.tsx

@@ -1,23 +0,0 @@
-import { ElDatePicker } from 'element-plus'
-import { VXETable } from 'vxe-table'
-
-// 日期区间选择渲染
-VXETable.renderer.add('XDataTimePicker', {
-  // 默认显示模板
-  renderItemContent(renderOpts, params) {
-    const { t } = useI18n()
-    const { data, field } = params
-    const { content } = renderOpts
-    return (
-      <ElDatePicker
-        v-model={data[field]}
-        style="width: 100%"
-        type={content ? (content as any) : 'datetimerange'}
-        value-format="YYYY-MM-DD HH:mm:ss"
-        range-separator="-"
-        start-placeholder={t('common.startTimeText')}
-        end-placeholder={t('common.endTimeText')}
-      ></ElDatePicker>
-    )
-  }
-})

+ 0 - 12
src/plugins/vxeTable/renderer/dict.tsx

@@ -1,12 +0,0 @@
-import { DictTag } from '@/components/DictTag'
-import { VXETable } from 'vxe-table'
-
-// 字典渲染
-VXETable.renderer.add('XDict', {
-  // 默认显示模板
-  renderDefault(renderOpts, params) {
-    const { row, column } = params
-    const { content } = renderOpts
-    return <DictTag type={content as unknown as string} value={row[column.field]}></DictTag>
-  }
-})

+ 0 - 10
src/plugins/vxeTable/renderer/html.tsx

@@ -1,10 +0,0 @@
-import { VXETable } from 'vxe-table'
-
-// 图片渲染
-VXETable.renderer.add('XHtml', {
-  // 默认显示模板
-  renderDefault(_renderOpts, params) {
-    const { row, column } = params
-    return <span v-html={row[column.field]}></span>
-  }
-})

+ 0 - 20
src/plugins/vxeTable/renderer/img.tsx

@@ -1,20 +0,0 @@
-import { VXETable } from 'vxe-table'
-import { ElImage } from 'element-plus'
-
-// 图片渲染
-VXETable.renderer.add('XImg', {
-  // 默认显示模板
-  renderDefault(_renderOpts, params) {
-    const { row, column } = params
-    return (
-      <ElImage
-        style="width: 80px; height: 50px"
-        src={row[column.field]}
-        key={row[column.field]}
-        preview-src-list={[row[column.field]]}
-        fit="contain"
-        lazy
-      ></ElImage>
-    )
-  }
-})

+ 0 - 7
src/plugins/vxeTable/renderer/index.tsx

@@ -1,7 +0,0 @@
-import './dataPicker'
-import './dataTimeRangePicker'
-import './dict'
-import './html'
-import './link'
-import './img'
-import './preview'

+ 0 - 15
src/plugins/vxeTable/renderer/link.tsx

@@ -1,15 +0,0 @@
-import { VXETable } from 'vxe-table'
-
-// 超链接渲染
-VXETable.renderer.add('XLink', {
-  // 默认显示模板
-  renderDefault(renderOpts, params) {
-    const { row, column } = params
-    const { events = {} } = renderOpts
-    return (
-      <a class="link" onClick={() => events.click(params)}>
-        {row[column.field]}
-      </a>
-    )
-  }
-})

+ 0 - 35
src/plugins/vxeTable/renderer/preview.tsx

@@ -1,35 +0,0 @@
-import { VXETable } from 'vxe-table'
-import { ElImage, ElLink } from 'element-plus'
-
-// 图片渲染
-VXETable.renderer.add('XPreview', {
-  // 默认显示模板
-  renderDefault(_renderOpts, params) {
-    const { row, column } = params
-    if (row.type.indexOf('image/') === 0) {
-      return (
-        <ElImage
-          style="width: 80px; height: 50px"
-          src={row[column.field]}
-          key={row[column.field]}
-          preview-src-list={[row[column.field]]}
-          fit="contain"
-          lazy
-        ></ElImage>
-      )
-    } else if (row.type.indexOf('video/') === 0) {
-      return (
-        <video>
-          <source src={row[column.field]}></source>
-        </video>
-      )
-    } else {
-      return (
-        // @ts-ignore
-        <ElLink href={row[column.field]} target="_blank">
-          {row[column.field]}
-        </ElLink>
-      )
-    }
-  }
-})

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

@@ -120,8 +120,6 @@ declare module '@vue/runtime-core' {
     VerifyPoints: typeof import('./../components/Verifition/src/Verify/VerifyPoints.vue')['default']
     VerifySlide: typeof import('./../components/Verifition/src/Verify/VerifySlide.vue')['default']
     XButton: typeof import('./../components/XButton/src/XButton.vue')['default']
-    XModal: typeof import('./../components/XModal/src/XModal.vue')['default']
-    XTable: typeof import('./../components/XTable/src/XTable.vue')['default']
     XTextButton: typeof import('./../components/XButton/src/XTextButton.vue')['default']
   }
   export interface ComponentCustomProperties {

+ 0 - 1
src/types/auto-imports.d.ts

@@ -6,7 +6,6 @@ export {}
 declare global {
   const DICT_TYPE: typeof import('@/utils/dict')['DICT_TYPE']
   const EffectScope: typeof import('vue')['EffectScope']
-  const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
   const computed: typeof import('vue')['computed']
   const createApp: typeof import('vue')['createApp']
   const customRef: typeof import('vue')['customRef']

+ 4 - 0
src/utils/constants.ts

@@ -114,6 +114,10 @@ export const PayChannelEnum = {
   ALIPAY_QR: {
     code: 'alipay_qr',
     name: '支付宝扫码支付'
+  },
+  ALIPAY_BAR: {
+    code: 'alipay_bar',
+    name: '支付宝条码支付'
   }
 }
 

+ 2 - 1
src/utils/dict.ts

@@ -21,7 +21,7 @@ export interface DictDataType {
 }
 
 export const getDictOptions = (dictType: string) => {
-  return dictStore.getDictByType(dictType)
+  return dictStore.getDictByType(dictType) || []
 }
 
 export const getIntDictOptions = (dictType: string) => {
@@ -117,6 +117,7 @@ export enum DICT_TYPE {
   INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status',
   INFRA_CONFIG_TYPE = 'infra_config_type',
   INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type',
+  INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type',
   INFRA_CODEGEN_SCENE = 'infra_codegen_scene',
   INFRA_FILE_STORAGE = 'infra_file_storage',
 

+ 3 - 3
src/views/bpm/oa/leave/detail.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog title="详情" v-model="modelVisible" :scroll="true" :max-height="200">
+  <Dialog title="详情" v-model="dialogVisible" :scroll="true" :max-height="200">
     <el-descriptions border :column="1">
       <el-descriptions-item label="请假类型">
         <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="detailData.type" />
@@ -21,13 +21,13 @@ import { DICT_TYPE } from '@/utils/dict'
 import { formatDate } from '@/utils/formatTime'
 import * as LeaveApi from '@/api/bpm/leave'
 
-const modelVisible = ref(false) // 弹窗的是否展示
+const dialogVisible = ref(false) // 弹窗的是否展示
 const detailLoading = ref(false) // 表单的加载中
 const detailData = ref() // 详情数据
 
 /** 打开弹窗 */
 const open = async (data: LeaveApi.LeaveVO) => {
-  modelVisible.value = true
+  dialogVisible.value = true
   // 设置数据
   detailLoading.value = true
   try {

+ 1 - 1
src/views/infra/codegen/EditTable.vue

@@ -30,7 +30,7 @@ const { query } = useRoute() // 查询参数
 const { delView } = useTagsViewStore() // 视图操作
 
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const activeName = ref('basicInfo') // Tag 激活的窗口
+const activeName = ref('colum') // Tag 激活的窗口
 const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
 const columInfoRef = ref<ComponentRef<typeof ColumInfoForm>>()
 const generateInfoRef = ref<ComponentRef<typeof GenerateInfoForm>>()

+ 1 - 1
src/views/infra/codegen/PreviewCode.vue

@@ -15,7 +15,7 @@
         v-loading="loading"
         element-loading-text="生成文件目录中..."
       >
-        <el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
+        <el-scrollbar height="calc(100vh - 88px - 40px)">
           <el-tree
             ref="treeRef"
             node-key="id"

+ 35 - 21
src/views/infra/codegen/components/GenerateInfoForm.vue

@@ -13,6 +13,19 @@
           </el-select>
         </el-form-item>
       </el-col>
+      <el-col :span="12">
+        <el-form-item prop="frontType" label="前端类型">
+          <el-select v-model="formData.frontType">
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="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">
@@ -25,6 +38,26 @@
           </el-select>
         </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="12">-->
       <!--        <el-form-item prop="packageName">-->
@@ -115,27 +148,6 @@
         </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>
@@ -297,6 +309,7 @@ const props = defineProps({
 const formRef = ref()
 const formData = ref({
   templateType: null,
+  frontType: null,
   scene: null,
   moduleName: '',
   businessName: '',
@@ -315,6 +328,7 @@ const formData = ref({
 
 const rules = reactive({
   templateType: [required],
+  frontType: [required],
   scene: [required],
   moduleName: [required],
   businessName: [required],

+ 176 - 172
src/views/mp/autoReply/index.vue

@@ -3,33 +3,16 @@
 
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
+    <el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px">
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" 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>
-        <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <!-- tab 切换 -->
   <ContentWrap>
-    <el-tabs v-model="type" @tab-change="handleTabChange">
+    <el-tabs v-model="msgType" @tab-change="handleTabChange">
       <!-- 操作工具栏 -->
       <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
@@ -38,26 +21,26 @@
             plain
             @click="handleAdd"
             v-hasPermi="['mp:auto-reply:create']"
-            v-if="type !== '1' || list.length <= 0"
+            v-if="msgType !== MsgType.Follow || list.length <= 0"
           >
             <Icon icon="ep:plus" />新增
           </el-button>
         </el-col>
       </el-row>
       <!-- tab 项 -->
-      <el-tab-pane name="1">
+      <el-tab-pane :name="MsgType.Follow">
         <template #label>
-          <span><Icon icon="ep:star-off" /> 关注时回复</span>
+          <span><Icon icon="ep:star" /> 关注时回复</span>
         </template>
       </el-tab-pane>
-      <el-tab-pane name="2">
+      <el-tab-pane :name="MsgType.Message">
         <template #label>
           <span><Icon icon="ep:chat-line-round" /> 消息回复</span>
         </template>
       </el-tab-pane>
-      <el-tab-pane name="3">
+      <el-tab-pane :name="MsgType.Keyword">
         <template #label>
-          <span><Icon icon="ep:news" /> 关键词回复</span>
+          <span><Icon icon="fa:newspaper-o" /> 关键词回复</span>
         </template>
       </el-tab-pane>
     </el-tabs>
@@ -67,10 +50,20 @@
         label="请求消息类型"
         align="center"
         prop="requestMessageType"
-        v-if="type === '2'"
+        v-if="msgType === MsgType.Message"
       />
-      <el-table-column label="关键词" align="center" prop="requestKeyword" v-if="type === '3'" />
-      <el-table-column label="匹配类型" align="center" prop="requestMatch" v-if="type === '3'">
+      <el-table-column
+        label="关键词"
+        align="center"
+        prop="requestKeyword"
+        v-if="msgType === MsgType.Keyword"
+      />
+      <el-table-column
+        label="匹配类型"
+        align="center"
+        prop="requestMatch"
+        v-if="msgType === MsgType.Keyword"
+      >
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
         </template>
@@ -84,7 +77,7 @@
         <template #default="scope">
           <div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
           <div v-else-if="scope.row.responseMessageType === 'voice'">
-            <WxVoicePlayer :url="scope.row.responseMediaUrl" />
+            <WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" />
           </div>
           <div v-else-if="scope.row.responseMessageType === 'image'">
             <a target="_blank" :href="scope.row.responseMediaUrl">
@@ -97,7 +90,11 @@
               scope.row.responseMessageType === 'shortvideo'
             "
           >
-            <WxVideoPlayer :url="scope.row.responseMediaUrl" style="margin-top: 10px" />
+            <WxVideoPlayer
+              v-if="scope.row.responseMediaUrl"
+              :url="scope.row.responseMediaUrl"
+              style="margin-top: 10px"
+            />
           </div>
           <div v-else-if="scope.row.responseMessageType === 'news'">
             <WxNews :articles="scope.row.responseArticles" />
@@ -143,21 +140,21 @@
     </el-table>
 
     <!-- 添加或修改自动回复的对话框 -->
-    <el-dialog :title="title" v-model="open" width="800px" append-to-body>
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="消息类型" prop="requestMessageType" v-if="type === '2'">
-          <el-select v-model="form.requestMessageType" placeholder="请选择">
+    <el-dialog :title="title" v-model="showReplyFormDialog" width="800px" append-to-body>
+      <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
+        <el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message">
+          <el-select v-model="replyForm.requestMessageType" placeholder="请选择">
             <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value">
               <el-option
-                v-if="requestMessageTypes.includes(dict.value)"
+                v-if="RequestMessageTypes.includes(dict.value)"
                 :label="dict.label"
                 :value="dict.value"
               />
             </template>
           </el-select>
         </el-form-item>
-        <el-form-item label="匹配类型" prop="requestMatch" v-if="type === '3'">
-          <el-select v-model="form.requestMatch" placeholder="请选择匹配类型" clearable>
+        <el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
+          <el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
             <el-option
               v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
               :key="dict.value"
@@ -166,8 +163,8 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="关键词" prop="requestKeyword" v-if="type === '3'">
-          <el-input v-model="form.requestKeyword" placeholder="请输入内容" clearable />
+        <el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
+          <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
         </el-form-item>
         <el-form-item label="回复消息">
           <WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" />
@@ -180,45 +177,47 @@
     </el-dialog>
   </ContentWrap>
 </template>
-<script setup name="MpAutoReply">
-import { ref, reactive, onMounted, nextTick } from 'vue'
+<script setup lang="ts" name="MpAutoReply">
 import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
 import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
 import WxMusic from '@/views/mp/components/wx-music/main.vue'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
 import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
-import { getSimpleAccountList } from '@/api/mp/account'
-import {
-  createAutoReply,
-  deleteAutoReply,
-  getAutoReply,
-  getAutoReplyPage,
-  updateAutoReply
-} from '@/api/mp/autoReply'
-
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import * as MpAutoReplyApi from '@/api/mp/autoReply'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { ContentWrap } from '@/components/ContentWrap'
+import type { TabPaneName } from 'element-plus'
 
 const message = useMessage()
 
-const queryFormRef = ref()
 const formRef = ref()
 
-// tab 类型(1、关注时回复;2、消息回复;3、关键词回复)
-const type = ref('3')
+// 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复)
+// 作为tab.name
+enum MsgType {
+  Follow = 1,
+  Message = 2,
+  Keyword = 3
+}
+const msgType = ref<MsgType>(MsgType.Keyword)
 // 允许选择的请求消息类型
-const requestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link']
+const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link']
 // 遮罩层
 const loading = ref(true)
-// 显示搜索条件
-// const showSearch = ref(true)
 // 总条数
 const total = ref(0)
 // 自动回复列表
-const list = ref([])
+const list = ref<any[]>([])
+
 // 查询参数
-const queryParams = reactive({
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   accountId: undefined
@@ -227,12 +226,50 @@ const queryParams = reactive({
 // 弹出层标题
 const title = ref('')
 // 是否显示弹出层
-const open = ref(false)
+const showReplyFormDialog = ref(false)
 // 表单参数
-const form = ref({})
+type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link'
+interface ReplyForm {
+  // relation:
+  id?: number
+  accountId?: number
+  type?: MsgType
+  // request:
+  requestMessageType?: ReplyType
+  requestMatch?: number
+  requestKeyword?: string
+  // response:
+  responseMessageType?: ReplyType
+  responseContent?: string
+  responseMediaId?: number
+  responseMediaUrl?: string
+  responseTitle?: string
+  responseDescription?: number
+  responseThumbMediaId?: string
+  responseThumbMediaUrl?: string
+  responseArticles?: any[]
+  responseMusicUrl?: string
+  responseHqMusicUrl?: string
+}
+interface ObjData {
+  type: ReplyType
+  accountId?: number
+  content?: string
+  mediaId?: number
+  url?: string
+  title?: string
+  description?: string
+  thumbMediaId?: number
+  thumbMediaUrl?: string
+  articles?: any[]
+  musicUrl?: string
+  hqMusicUrl?: string
+}
+const replyForm = ref<ReplyForm>({})
 // 回复消息
-const objData = ref({
-  type: 'text'
+const objData = ref<ObjData>({
+  type: 'text',
+  accountId: undefined
 })
 // 表单校验
 const rules = {
@@ -240,43 +277,27 @@ const rules = {
   requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }]
 }
 
-const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件,解决无法清除的问题
-
-// 公众号账号列表
-const accountList = ref([])
+// 重置 WxReplySelect 组件,解决无法清除的问题
+const hackResetWxReplySelect = ref(false)
 
-onMounted(() => {
-  getSimpleAccountList().then((data) => {
-    accountList.value = data
-    // 默认选中第一个
-    if (accountList.value.length > 0) {
-      queryParams.accountId = accountList.value[0].id
-    }
-    // 加载数据
-    getList()
-  })
-})
+const onAccountChanged = (id?: number) => {
+  queryParams.accountId = id
+  getList()
+}
 
 /** 查询列表 */
 const getList = async () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    message.error('未选中公众号,无法查询自动回复')
-    return false
-  }
-
   loading.value = false
-  // 处理查询参数
-  let params = {
-    ...queryParams,
-    type: type.value
-  }
-  // 执行查询
-  getAutoReplyPage(params).then((data) => {
+  try {
+    const data = await MpAutoReplyApi.getAutoReplyPage({
+      ...queryParams,
+      type: msgType.value
+    })
     list.value = data.list
     total.value = data.total
+  } finally {
     loading.value = false
-  })
+  }
 }
 
 /** 搜索按钮操作 */
@@ -285,18 +306,8 @@ const handleQuery = () => {
   getList()
 }
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    queryParams.accountId = accountList.value[0].id
-  }
-  handleQuery()
-}
-
-const handleTabChange = (tabName) => {
-  type.value = tabName
+const handleTabChange = (tabName: TabPaneName) => {
+  msgType.value = tabName as MsgType
   handleQuery()
 }
 
@@ -305,94 +316,87 @@ const handleAdd = () => {
   reset()
   resetEditor()
   // 打开表单,并设置初始化
-  open.value = true
-  title.value = '新增自动回复'
   objData.value = {
     type: 'text',
     accountId: queryParams.accountId
   }
+
+  title.value = '新增自动回复'
+  showReplyFormDialog.value = true
 }
 
 /** 修改按钮操作 */
-const handleUpdate = (row) => {
+const handleUpdate = async (row: any) => {
   reset()
   resetEditor()
-  console.log(row)
 
-  getAutoReply(row.id).then((data) => {
-    // 设置属性
-    form.value = { ...data }
-    delete form.value['responseMessageType']
-    delete form.value['responseContent']
-    delete form.value['responseMediaId']
-    delete form.value['responseMediaUrl']
-    delete form.value['responseDescription']
-    delete form.value['responseArticles']
-    objData.value = {
-      type: data.responseMessageType,
-      accountId: queryParams.accountId,
-      content: data.responseContent,
-      mediaId: data.responseMediaId,
-      url: data.responseMediaUrl,
-      title: data.responseTitle,
-      description: data.responseDescription,
-      thumbMediaId: data.responseThumbMediaId,
-      thumbMediaUrl: data.responseThumbMediaUrl,
-      articles: data.responseArticles,
-      musicUrl: data.responseMusicUrl,
-      hqMusicUrl: data.responseHqMusicUrl
-    }
+  const data = await MpAutoReplyApi.getAutoReply(row.id)
+  // 设置属性
+  replyForm.value = { ...data }
+  delete replyForm.value['responseMessageType']
+  delete replyForm.value['responseContent']
+  delete replyForm.value['responseMediaId']
+  delete replyForm.value['responseMediaUrl']
+  delete replyForm.value['responseDescription']
+  delete replyForm.value['responseArticles']
+  objData.value = {
+    type: data.responseMessageType,
+    accountId: queryParams.accountId,
+    content: data.responseContent,
+    mediaId: data.responseMediaId,
+    url: data.responseMediaUrl,
+    title: data.responseTitle,
+    description: data.responseDescription,
+    thumbMediaId: data.responseThumbMediaId,
+    thumbMediaUrl: data.responseThumbMediaUrl,
+    articles: data.responseArticles,
+    musicUrl: data.responseMusicUrl,
+    hqMusicUrl: data.responseHqMusicUrl
+  }
 
-    // 打开表单
-    open.value = true
-    title.value = '修改自动回复'
-  })
+  // 打开表单
+  title.value = '修改自动回复'
+  showReplyFormDialog.value = true
 }
 
-const handleSubmit = () => {
-  formRef.value?.validate((valid) => {
-    if (!valid) {
-      return
-    }
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
 
-    // 处理回复消息
-    const form = { ...form.value }
-    form.responseMessageType = objData.value.type
-    form.responseContent = objData.value.content
-    form.responseMediaId = objData.value.mediaId
-    form.responseMediaUrl = objData.value.url
-    form.responseTitle = objData.value.title
-    form.responseDescription = objData.value.description
-    form.responseThumbMediaId = objData.value.thumbMediaId
-    form.responseThumbMediaUrl = objData.value.thumbMediaUrl
-    form.responseArticles = objData.value.articles
-    form.responseMusicUrl = objData.value.musicUrl
-    form.responseHqMusicUrl = objData.value.hqMusicUrl
+  // 处理回复消息
+  const submitForm: any = { ...replyForm.value }
+  submitForm.responseMessageType = objData.value.type
+  submitForm.responseContent = objData.value.content
+  submitForm.responseMediaId = objData.value.mediaId
+  submitForm.responseMediaUrl = objData.value.url
+  submitForm.responseTitle = objData.value.title
+  submitForm.responseDescription = objData.value.description
+  submitForm.responseThumbMediaId = objData.value.thumbMediaId
+  submitForm.responseThumbMediaUrl = objData.value.thumbMediaUrl
+  submitForm.responseArticles = objData.value.articles
+  submitForm.responseMusicUrl = objData.value.musicUrl
+  submitForm.responseHqMusicUrl = objData.value.hqMusicUrl
 
-    if (form.value.id !== undefined) {
-      updateAutoReply(form).then(() => {
-        message.success('修改成功')
-        open.value = false
-        getList()
-      })
-    } else {
-      createAutoReply(form).then(() => {
-        message.success('新增成功')
-        open.value = false
-        getList()
-      })
-    }
-  })
+  if (replyForm.value.id !== undefined) {
+    await MpAutoReplyApi.updateAutoReply(submitForm)
+    message.success('修改成功')
+  } else {
+    await MpAutoReplyApi.createAutoReply(submitForm)
+    message.success('新增成功')
+  }
+
+  showReplyFormDialog.value = false
+  getList()
 }
 
 // 表单重置
 const reset = () => {
-  form.value = {
+  replyForm.value = {
     id: undefined,
     accountId: queryParams.accountId,
-    type: type.value,
+    type: msgType.value,
     requestKeyword: undefined,
-    requestMatch: type.value === '3' ? 1 : undefined,
+    requestMatch: msgType.value === MsgType.Keyword ? 1 : undefined,
     requestMessageType: undefined
   }
   formRef.value?.resetFields()
@@ -400,7 +404,7 @@ const reset = () => {
 
 // 取消按钮
 const cancel = () => {
-  open.value = false
+  showReplyFormDialog.value = false
   reset()
 }
 
@@ -414,7 +418,7 @@ const resetEditor = () => {
 
 const handleDelete = async (row) => {
   await message.confirm('是否确认删除此数据?')
-  await deleteAutoReply(row.id)
+  await MpAutoReplyApi.deleteAutoReply(row.id)
   await getList()
   message.success('删除成功')
 }

+ 36 - 0
src/views/mp/components/WxMpSelect.vue

@@ -0,0 +1,36 @@
+<template>
+  <el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged">
+    <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
+  </el-select>
+</template>
+<!-- TODO @芋艿:WxMpSelect 改成 WxAccountSelect,然后挪到现有的 wx-account-select 包下 -->
+<script lang="ts" setup name="WxMpSelect">
+import * as MpAccountApi from '@/api/mp/account'
+
+const account: MpAccountApi.AccountVO = reactive({
+  id: undefined,
+  name: ''
+})
+const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
+
+const emit = defineEmits<{
+  (e: 'change', id?: number, name?: string): void
+}>()
+
+onMounted(() => {
+  handleQuery()
+})
+
+const handleQuery = async () => {
+  accountList.value = await MpAccountApi.getSimpleAccountList()
+  // 默认选中第一个
+  if (accountList.value.length > 0) {
+    account.id = accountList.value[0].id
+    emit('change', account.id, account.name)
+  }
+}
+
+const onChanged = () => {
+  emit('change', account.id, account.name)
+}
+</script>

+ 44 - 0
src/views/mp/components/wx-account-select/main.vue

@@ -0,0 +1,44 @@
+<template>
+  <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
+    <el-form-item label="公众号" prop="accountId">
+      <!-- TODO 芋艿:需要将 el-form 和 el-select 解耦 -->
+      <el-select
+        v-model="accountId"
+        placeholder="请选择公众号"
+        class="!w-240px"
+        @change="accountChanged()"
+      >
+        <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
+      </el-select>
+    </el-form-item>
+    <el-form-item>
+      <slot name="actions"></slot>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup name="WxAccountSelect">
+import * as MpAccountApi from '@/api/mp/account'
+const accountId = ref()
+const accountList = ref([])
+const queryFormRef = ref()
+
+const emit = defineEmits(['change'])
+
+onMounted(() => {
+  handleQuery()
+})
+
+const handleQuery = async () => {
+  accountList.value = await MpAccountApi.getSimpleAccountList()
+  // 默认选中第一个
+  if (accountList.value.length > 0) {
+    accountId.value = accountList.value[0].id
+    emit('change', accountId.value)
+  }
+}
+
+const accountChanged = () => {
+  emit('change', accountId.value)
+}
+</script>

+ 9 - 106
src/views/mp/components/wx-editor/WxEditor.vue

@@ -1,11 +1,11 @@
 <script setup>
 import { ref, reactive } from 'vue'
-import { QuillEditor } from '@vueup/vue-quill'
-import '@vueup/vue-quill/dist/vue-quill.snow.css'
 import { getAccessToken } from '@/utils/auth'
-import editorOptions from './quill-options'
+import { Editor } from '@/components/Editor'
 
 const BASE_URL = import.meta.env.VITE_BASE_URL
+const actionUrl = BASE_URL + '/admin-api/mp/material/upload-news-image' // 这里写你要上传的图片服务器地址
+const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
 
 const message = useMessage()
 
@@ -30,21 +30,16 @@ const props = defineProps({
 const emit = defineEmits(['input'])
 
 const myQuillEditorRef = ref()
-
 const content = ref(props.value.replace(/data-src/g, 'src'))
-
 const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
-
-const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-news-image') // 这里写你要上传的图片服务器地址
-const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部
 const uploadData = reactive({
   type: 'image', // TODO 芋艿:试试要不要换成 thumb
   accountId: props.accountId
 })
 
-const onEditorChange = () => {
+const onEditorChange = (text) => {
   //内容改变事件
-  emit('input', content.value)
+  emit('input', text)
 }
 
 // 富文本图片上传前
@@ -98,104 +93,12 @@ const uploadError = () => {
         :on-error="uploadError"
         :before-upload="beforeUpload"
       />
-      <QuillEditor
-        class="editor"
-        v-model="content"
+      <Editor
+        editor-id="wxEditor"
         ref="quillEditorRef"
-        :options="editorOptions"
-        @change="onEditorChange($event)"
+        :modelValue="content"
+        @change="(editor) => onEditorChange(editor.getText())"
       />
     </div>
   </div>
 </template>
-
-<style>
-.editor {
-  line-height: normal !important;
-  height: 500px;
-}
-
-.ql-snow .ql-tooltip[data-mode='link']::before {
-  content: '请输入链接地址:';
-}
-
-.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
-  border-right: 0;
-  content: '保存';
-  padding-right: 0;
-}
-
-.ql-snow .ql-tooltip[data-mode='video']::before {
-  content: '请输入视频地址:';
-}
-
-.ql-snow .ql-picker.ql-size .ql-picker-label::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item::before {
-  content: '14px';
-}
-
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
-  content: '10px';
-}
-
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
-  content: '18px';
-}
-
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
-  content: '32px';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item::before {
-  content: '文本';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
-  content: '标题1';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
-  content: '标题2';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
-  content: '标题3';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
-  content: '标题4';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
-  content: '标题5';
-}
-
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
-  content: '标题6';
-}
-
-.ql-snow .ql-picker.ql-font .ql-picker-label::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item::before {
-  content: '标准字体';
-}
-
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
-  content: '衬线字体';
-}
-
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
-  content: '等宽字体';
-}
-</style>

+ 1 - 1
src/views/mp/components/wx-location/main.vue

@@ -31,7 +31,6 @@
           />
         </el-row>
         <el-row>
-          <el-icon><Location /></el-icon>
           <Icon icon="ep:location" />
           {{ label }}
         </el-row>
@@ -39,6 +38,7 @@
     </el-link>
   </div>
 </template>
+
 <script setup lang="ts" name="WxLocation">
 const props = defineProps({
   locationX: {

+ 91 - 109
src/views/mp/components/wx-material-select/main.vue

@@ -14,7 +14,8 @@
           <p class="item-name">{{ item.name }}</p>
           <el-row class="ope-row">
             <el-button type="success" @click="selectMaterialFun(item)">
-              选择 <Icon icon="ep:circle-check" />
+              选择
+              <Icon icon="ep:circle-check" />
             </el-button>
           </el-row>
         </div>
@@ -48,7 +49,8 @@
         <el-table-column label="操作" align="center" fixed="right">
           <template #default="scope">
             <el-button type="primary" link @click="selectMaterialFun(scope.row)"
-              >选择<Icon icon="ep:plus" />
+              >选择
+              <Icon icon="ep:plus" />
             </el-button>
           </template>
         </el-table-column>
@@ -89,7 +91,8 @@
         >
           <template #default="scope">
             <el-button type="primary" link @click="selectMaterialFun(scope.row)"
-              >选择<Icon icon="akar-icons:circle-plus" />
+              >选择
+              <Icon icon="akar-icons:circle-plus" />
             </el-button>
           </template>
         </el-table-column>
@@ -110,7 +113,8 @@
             <WxNews :articles="item.content.newsItem" />
             <el-row class="ope-row">
               <el-button type="success" @click="selectMaterialFun(item)">
-                选择<Icon icon="ep:circle-check" />
+                选择
+                <Icon icon="ep:circle-check" />
               </el-button>
             </el-row>
           </div>
@@ -127,125 +131,101 @@
   </div>
 </template>
 
-<script lang="ts" name="WxMaterialSelect">
+<script lang="ts" setup 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 * as MpMaterialApi from '@/api/mp/material'
+import * as MpFreePublishApi from '@/api/mp/freePublish'
+import * as MpDraftApi from '@/api/mp/draft'
 import { dateFormatter } from '@/utils/formatTime'
-import { defineComponent, PropType } from 'vue'
 
-export default defineComponent({
-  components: {
-    WxNews,
-    WxVoicePlayer,
-    WxVideoPlayer
+const props = defineProps({
+  objData: {
+    type: Object, // type - 类型;accountId - 公众号账号编号
+    required: true
   },
-  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)
+  newsType: {
+    // 图文类型:1、已发布图文;2、草稿箱图文
+    type: String as PropType<string>,
+    default: '1'
+  }
+})
 
-    const selectMaterialFun = (item) => {
-      ctx.emit('select-material', 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 emit = defineEmits(['select-material'])
 
-    const getMaterialPageFun = async () => {
-      let data = await getMaterialPage({
-        ...queryParams,
-        type: objDataRef.type
-      })
-      list.value = data.list
-      total.value = data.total
-      loading.value = false
-    }
+// 遮罩层
+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 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 selectMaterialFun = (item) => {
+  emit('select-material', item)
+}
 
-    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
+const getPage = async () => {
+  loading.value = true
+  try {
+    if (objDataRef.type === 'news' && newsTypeRef.value === '1') {
+      // 【图文】+ 【已发布】
+      await getFreePublishPageFun()
+    } else if (objDataRef.type === 'news' && newsTypeRef.value === '2') {
+      // 【图文】+ 【草稿】
+      await getDraftPageFun()
+    } else {
+      // 【素材】
+      await getMaterialPageFun()
     }
+  } finally {
+    loading.value = false
+  }
+}
+
+const getMaterialPageFun = async () => {
+  const data = await MpMaterialApi.getMaterialPage({
+    ...queryParams,
+    type: objDataRef.type
+  })
+  list.value = data.list
+  total.value = data.total
+}
 
-    onMounted(async () => {
-      getPage()
+const getFreePublishPageFun = async () => {
+  const data = await MpFreePublishApi.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
+}
 
-    return {
-      handleQuery,
-      dateFormatter,
-      selectMaterialFun,
-      getMaterialPageFun,
-      getPage,
-      formatDate,
-      queryParams,
-      objDataRef,
-      list,
-      total,
-      loading
-    }
-  }
+const getDraftPageFun = async () => {
+  const data = await MpDraftApi.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
+}
+
+onMounted(async () => {
+  getPage()
 })
 </script>
 <style lang="scss" scoped>
@@ -276,6 +256,7 @@ p {
   .waterfall {
     column-count: 3;
   }
+
   p {
     color: red;
   }
@@ -285,6 +266,7 @@ p {
   .waterfall {
     column-count: 2;
   }
+
   p {
     color: orange;
   }

+ 181 - 178
src/views/mp/components/wx-msg/main.vue

@@ -39,79 +39,79 @@
               :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''"
             >
               <!-- 【事件】区域 -->
-              <div v-if="item.type === 'event' && item.event === 'subscribe'">
+              <div v-if="item.type === MsgType.Event && item.event === 'subscribe'">
                 <el-tag type="success">关注</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'unsubscribe'">
                 <el-tag type="danger">取消关注</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'CLICK'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'CLICK'">
                 <el-tag>点击菜单</el-tag>
                 【{{ item.eventKey }}】
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'VIEW'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'VIEW'">
                 <el-tag>点击菜单链接</el-tag>
                 【{{ item.eventKey }}】
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_waitmsg'">
                 <el-tag>扫码结果</el-tag>
                 【{{ item.eventKey }}】
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'scancode_push'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_push'">
                 <el-tag>扫码结果</el-tag>
                 【{{ item.eventKey }}】
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_sysphoto'">
                 <el-tag>系统拍照发图</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_photo_or_album'">
                 <el-tag>拍照或者相册</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_weixin'">
                 <el-tag>微信相册</el-tag>
               </div>
-              <div v-else-if="item.type === 'event' && item.event === 'location_select'">
+              <div v-else-if="item.type === MsgType.Event && item.event === 'location_select'">
                 <el-tag>选择地理位置</el-tag>
               </div>
-              <div v-else-if="item.type === 'event'">
+              <div v-else-if="item.type === MsgType.Event">
                 <el-tag type="danger">未知事件类型</el-tag>
               </div>
               <!-- 【消息】区域 -->
-              <div v-else-if="item.type === 'text'">{{ item.content }}</div>
-              <div v-else-if="item.type === 'voice'">
-                <wx-voice-player :url="item.mediaUrl" :content="item.recognition" />
+              <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
+              <div v-else-if="item.type === MsgType.Voice">
+                <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
               </div>
-              <div v-else-if="item.type === 'image'">
+              <div v-else-if="item.type === MsgType.Image">
                 <a target="_blank" :href="item.mediaUrl">
                   <img :src="item.mediaUrl" style="width: 100px" />
                 </a>
               </div>
               <div
-                v-else-if="item.type === 'video' || item.type === 'shortvideo'"
+                v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'"
                 style="text-align: center"
               >
-                <wx-video-player :url="item.mediaUrl" />
+                <WxVideoPlayer :url="item.mediaUrl" />
               </div>
-              <div v-else-if="item.type === 'link'" class="avue-card__detail">
+              <div v-else-if="item.type === MsgType.Link" class="avue-card__detail">
                 <el-link type="success" :underline="false" target="_blank" :href="item.url">
                   <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
                 </el-link>
                 <div class="avue-card__info" style="height: unset">{{ item.description }}</div>
               </div>
               <!-- TODO 芋艿:待完善 -->
-              <div v-else-if="item.type === 'location'">
-                <wx-location
+              <div v-else-if="item.type === MsgType.Location">
+                <WxLocation
                   :label="item.label"
                   :location-y="item.locationY"
                   :location-x="item.locationX"
                 />
               </div>
-              <div v-else-if="item.type === 'news'" style="width: 300px">
+              <div v-else-if="item.type === MsgType.News" style="width: 300px">
                 <!-- TODO 芋艿:待测试;详情页也存在类似的情况 -->
-                <wx-news :articles="item.articles" />
+                <WxNews :articles="item.articles" />
               </div>
-              <div v-else-if="item.type === 'music'">
-                <wx-music
+              <div v-else-if="item.type === MsgType.Music">
+                <WxMusic
                   :title="item.title"
                   :description="item.description"
                   :thumb-media-url="item.thumbMediaUrl"
@@ -125,182 +125,185 @@
       </div>
     </div>
     <div class="msg-send" v-loading="sendLoading">
-      <wx-reply-select ref="replySelect" :objData="objData" />
+      <WxReplySelect ref="replySelectRef" :objData="objData" />
       <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
     </div>
   </ContentWrap>
 </template>
 
-<script lang="ts" name="WxMsg">
-import { getMessagePage, sendMessage } from '@/api/mp/message'
+<script setup lang="ts" name="WxMsg">
 import WxReplySelect from '@/views/mp/components/wx-reply/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 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 { getMessagePage, sendMessage } from '@/api/mp/message'
 import { getUser } from '@/api/mp/user'
-import { defineComponent } from 'vue'
-
-const message = useMessage() // 消息弹窗
+import { formatDate } from '@/utils/formatTime'
 import profile from '@/assets/imgs/profile.jpg'
 import wechat from '@/assets/imgs/wechat.png'
-import { formatDate } from '@/utils/formatTime'
+import { MsgType } from './types'
 
-export default defineComponent({
-  components: {
-    WxReplySelect,
-    WxVideoPlayer,
-    WxVoicePlayer,
-    WxNews,
-    WxLocation,
-    WxMusic
-  },
-  props: {
-    userId: {
-      type: Number,
-      required: true
-    }
-  },
-  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
-    })
+const message = useMessage() // 消息弹窗
 
-    // ========= 消息发送 =========
-    const sendLoading = ref(false) // 发送消息是否加载中
-    const objData = reactive({
-      // 微信发送消息
-      type: 'text',
-      accountId: null,
-      articles: []
-    })
+const props = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
 
-    const replySelect = ref(null)
-    // 执行发送
-    const sendMsg = async () => {
-      if (!objData) {
-        return
-      }
-      //     // 公众号限制:客服消息,公众号只允许发送一条
-      if (objData.type === 'news' && objData.articles.length > 1) {
-        objData.articles = [objData.articles[0]]
-        message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
-      }
-      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: props.userId,
-            accountId: page.accountId
-          },
-          params
-        )
-      )
-      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
-            }
-          }
-        })
-      }
-    }
-    const refreshChange = () => {
-      getPage(queryParams, null)
-    }
-    /** 定位到消息底部 */
-    const scrollToBottom = () => {
-      nextTick(() => {
+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
+})
+
+interface User {
+  nickname: string
+  avatar: string
+  accountId: number
+}
+// 由于微信不再提供昵称,直接使用“用户”展示
+const user: User = reactive({
+  nickname: '用户',
+  avatar: profile,
+  accountId: 0 // 公众号账号编号
+})
+
+interface Mp {
+  nickname: string
+  avatar: string
+}
+const mp: Mp = reactive({
+  nickname: '公众号',
+  avatar: wechat
+})
+
+// ========= 消息发送 =========
+const sendLoading = ref(false) // 发送消息是否加载中
+interface ObjData {
+  type: MsgType
+  accountId: number | null
+  articles: any[]
+}
+
+// 微信发送消息
+const objData: ObjData = reactive({
+  type: MsgType.Text,
+  accountId: null,
+  articles: []
+})
+
+const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null)
+
+/** 完成加载 */
+onMounted(async () => {
+  const data = await getUser(props.userId)
+  user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname
+  user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar
+  user.accountId = data.accountId
+  queryParams.accountId = data.accountId
+  objData.accountId = data.accountId
+
+  refreshChange()
+})
+
+// 执行发送
+const sendMsg = async () => {
+  if (!objData) {
+    return
+  }
+  // 公众号限制:客服消息,公众号只允许发送一条
+  if (objData.type === MsgType.News && objData.articles.length > 1) {
+    objData.articles = [objData.articles[0]]
+    message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
+  }
+
+  const data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
+  sendLoading.value = false
+
+  list.value = [...list.value, ...[data]]
+  scrollToBottom()
+
+  //ts检查的時候会判断这个组件可能是空的,所以需要进行断言。
+  //避免 tab 的数据未清理
+  const deleteObj = replySelectRef.value?.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: props.userId,
+        accountId: page.accountId
+      },
+      params
+    )
+  )
+
+  const msgDiv = document.getElementById('msg-div' + nowStr.value)
+  let scrollHeight = 0
+  if (msgDiv) {
+    scrollHeight = msgDiv.scrollHeight
+  }
+  // 处理数据
+  const 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) {
-          div.scrollTop = div.scrollHeight
+        if (div && msgDiv) {
+          msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
         }
-      })
-    }
-
-    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,
-      formatDate,
-      scrollToBottom,
-      objData,
-      mp,
-      user,
-      queryParams,
-      list,
-      loadMore,
-      loading,
-      nowStr,
-      sendLoading
-    }
   }
-})
+}
+
+const refreshChange = () => {
+  getPage(queryParams, null)
+}
+
+/** 定位到消息底部 */
+const scrollToBottom = () => {
+  nextTick(() => {
+    let div = document.getElementById('msg-div' + nowStr.value)
+    if (div) {
+      div.scrollTop = div.scrollHeight
+    }
+  })
+}
 </script>
+
 <style lang="scss" scoped>
 /* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc  */
 @import './comment.scss';

+ 11 - 0
src/views/mp/components/wx-msg/types.ts

@@ -0,0 +1,11 @@
+export enum MsgType {
+  Event = 'event',
+  Text = 'text',
+  Voice = 'voice',
+  Image = 'image',
+  Video = 'video',
+  Link = 'link',
+  Location = 'location',
+  Music = 'music',
+  News = 'news'
+}

+ 224 - 275
src/views/mp/draft/index.vue

@@ -11,18 +11,9 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号">
-          <el-option
-            v-for="item in accountList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
         <el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']">
           <Icon icon="ep:plus" />新增
         </el-button>
@@ -35,7 +26,7 @@
     <div class="waterfall" v-loading="loading">
       <template v-for="item in list" :key="item.articleId">
         <div class="waterfall-item" v-if="item.content && item.content.newsItem">
-          <wx-news :articles="item.content.newsItem" />
+          <WxNews :articles="item.content.newsItem" />
           <!-- 操作按钮 -->
           <el-row class="ope-row">
             <el-button
@@ -75,215 +66,220 @@
     />
   </ContentWrap>
 
-  <!-- TODO @Dhb52:迁移成独立路由 -->
   <div class="app-container">
     <!-- 添加或修改草稿对话框 -->
-    <Teleport to="body">
-      <el-dialog
-        :title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
-        width="80%"
-        top="20px"
-        v-model="dialogNewsVisible"
-        :before-close="dialogNewsClose"
-        :close-on-click-modal="false"
-      >
-        <div class="left">
-          <div class="select-item">
-            <div v-for="(news, index) in articlesAdd" :key="news.id">
-              <div
-                class="news-main father"
-                v-if="index === 0"
-                :class="{ activeAddNews: isActiveAddNews === index }"
-                @click="activeNews(index)"
-              >
-                <div class="news-content">
-                  <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" />
-                  <div class="news-content-title">{{ news.title }}</div>
-                </div>
-                <div class="child" v-if="articlesAdd.length > 1">
-                  <el-button size="small" @click="downNews(index)"
-                    ><Icon icon="ep:sort-down" />下移</el-button
-                  >
-                  <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)"
-                    ><Icon icon="ep:delete" />删除
-                  </el-button>
-                </div>
+    <el-dialog
+      :title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
+      width="80%"
+      top="20px"
+      v-model="dialogNewsVisible"
+      :before-close="dialogNewsClose"
+      :close-on-click-modal="false"
+    >
+      <div class="left">
+        <div class="select-item">
+          <div v-for="(news, index) in articlesAdd" :key="news.id">
+            <div
+              class="news-main father"
+              v-if="index === 0"
+              :class="{ activeAddNews: isActiveAddNews === index }"
+              @click="activeNews(index)"
+            >
+              <div class="news-content">
+                <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" />
+                <div class="news-content-title">{{ news.title }}</div>
               </div>
-              <div
-                class="news-main-item father"
-                v-if="index > 0"
-                :class="{ activeAddNews: isActiveAddNews === index }"
-                @click="activeNews(index)"
-              >
-                <div class="news-content-item">
-                  <div class="news-content-item-title">{{ news.title }}</div>
-                  <div class="news-content-item-img">
-                    <img
-                      class="material-img"
-                      v-if="news.thumbUrl"
-                      :src="news.thumbUrl"
-                      height="100%"
-                    />
-                  </div>
-                </div>
-                <div class="child">
-                  <el-button
-                    v-if="articlesAdd.length > index + 1"
-                    size="small"
-                    @click="downNews(index)"
-                    ><Icon icon="ep:sort-down" />下移
-                  </el-button>
-                  <el-button size="small" @click="upNews(index)"
-                    ><Icon icon="ep:sort-up" />上移</el-button
-                  >
-                  <el-button
-                    v-if="operateMaterial === 'add'"
-                    type="danger"
-                    size="small"
-                    @click="minusNews(index)"
-                    ><Icon icon="ep:delete" />删除
-                  </el-button>
+              <div class="child" v-if="articlesAdd.length > 1">
+                <el-button size="small" @click="downNews(index)"
+                  ><Icon icon="ep:sort-down" />下移</el-button
+                >
+                <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)"
+                  ><Icon icon="ep:delete" />删除
+                </el-button>
+              </div>
+            </div>
+            <div
+              class="news-main-item father"
+              v-if="index > 0"
+              :class="{ activeAddNews: isActiveAddNews === index }"
+              @click="activeNews(index)"
+            >
+              <div class="news-content-item">
+                <div class="news-content-item-title">{{ news.title }}</div>
+                <div class="news-content-item-img">
+                  <img
+                    class="material-img"
+                    v-if="news.thumbUrl"
+                    :src="news.thumbUrl"
+                    height="100%"
+                  />
                 </div>
               </div>
+              <div class="child">
+                <el-button
+                  v-if="articlesAdd.length > index + 1"
+                  size="small"
+                  @click="downNews(index)"
+                  ><Icon icon="ep:sort-down" />下移
+                </el-button>
+                <el-button size="small" @click="upNews(index)"
+                  ><Icon icon="ep:sort-up" />上移</el-button
+                >
+                <el-button
+                  v-if="operateMaterial === 'add'"
+                  type="danger"
+                  size="small"
+                  @click="minusNews(index)"
+                  ><Icon icon="ep:delete" />删除
+                </el-button>
+              </div>
             </div>
-            <el-row justify="center" class="ope-row">
-              <el-button
-                type="primary"
-                circle
-                @click="plusNews(item)"
-                v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
-              >
-                <Icon icon="ep:plus" />
-              </el-button>
-            </el-row>
           </div>
+          <el-row justify="center" class="ope-row">
+            <el-button
+              type="primary"
+              circle
+              @click="plusNews"
+              v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
+            >
+              <Icon icon="ep:plus" />
+            </el-button>
+          </el-row>
         </div>
-        <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
-          <br />
-          <br />
-          <br />
-          <br />
-          <!-- 标题、作者、原文地址 -->
-          <el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题(必填)" />
-          <el-input
-            v-model="articlesAdd[isActiveAddNews].author"
-            placeholder="请输入作者"
-            style="margin-top: 5px"
-          />
-          <el-input
-            v-model="articlesAdd[isActiveAddNews].contentSourceUrl"
-            placeholder="请输入原文地址"
-            style="margin-top: 5px"
-          />
-          <!-- 封面和摘要 -->
-          <div class="input-tt">封面和摘要:</div>
-          <div>
-            <div class="thumb-div">
-              <img
-                class="material-img"
-                v-if="articlesAdd[isActiveAddNews].thumbUrl"
-                :src="articlesAdd[isActiveAddNews].thumbUrl"
-                :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
-              />
-              <Icon
-                v-else
-                icon="ep:plus"
-                class="avatar-uploader-icon"
-                :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
-              />
-              <div class="thumb-but">
-                <el-upload
-                  :action="actionUrl"
-                  :headers="headers"
-                  multiple
-                  :limit="1"
-                  :file-list="fileList"
-                  :data="uploadData"
-                  :before-upload="beforeThumbImageUpload"
-                  :on-success="handleUploadSuccess"
+      </div>
+      <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
+        <br />
+        <br />
+        <br />
+        <br />
+        <!-- 标题、作者、原文地址 -->
+        <el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题(必填)" />
+        <el-input
+          v-model="articlesAdd[isActiveAddNews].author"
+          placeholder="请输入作者"
+          style="margin-top: 5px"
+        />
+        <el-input
+          v-model="articlesAdd[isActiveAddNews].contentSourceUrl"
+          placeholder="请输入原文地址"
+          style="margin-top: 5px"
+        />
+        <!-- 封面和摘要 -->
+        <div class="input-tt">封面和摘要:</div>
+        <div>
+          <div class="thumb-div">
+            <img
+              class="material-img"
+              v-if="articlesAdd[isActiveAddNews].thumbUrl"
+              :src="articlesAdd[isActiveAddNews].thumbUrl"
+              :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
+            />
+            <Icon
+              v-else
+              icon="ep:plus"
+              class="avatar-uploader-icon"
+              :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
+            />
+            <div class="thumb-but">
+              <el-upload
+                :action="uploadUrl"
+                :headers="headers"
+                multiple
+                :limit="1"
+                :file-list="fileList"
+                :data="uploadData"
+                :before-upload="beforeThumbImageUpload"
+                :on-success="handleUploadSuccess"
+              >
+                <template #trigger>
+                  <el-button size="small" type="primary">本地上传</el-button>
+                </template>
+                <el-button
+                  size="small"
+                  type="primary"
+                  @click="openMaterial"
+                  style="margin-left: 5px"
+                  >素材库选择</el-button
                 >
-                  <template #trigger>
-                    <el-button size="small" type="primary">本地上传</el-button>
-                  </template>
-                  <el-button
-                    size="small"
-                    type="primary"
-                    @click="openMaterial"
-                    style="margin-left: 5px"
-                    >素材库选择</el-button
-                  >
-                  <template #tip>
-                    <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div>
-                  </template>
-                </el-upload>
-              </div>
-              <Teleport to="body">
-                <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%">
-                  <WxMaterialSelect
-                    ref="materialSelectRef"
-                    :objData="{ type: 'image', accountId: queryParams.accountId }"
-                    @select-material="selectMaterial"
-                  />
-                </el-dialog>
-              </Teleport>
+                <template #tip>
+                  <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div>
+                </template>
+              </el-upload>
             </div>
-            <el-input
-              :rows="8"
-              type="textarea"
-              v-model="articlesAdd[isActiveAddNews].digest"
-              placeholder="请输入摘要"
-              class="digest"
-              maxlength="120"
-              style="float: right"
-            />
+            <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%" append-to-body>
+              <WxMaterialSelect
+                ref="materialSelectRef"
+                :objData="{ type: 'image', accountId: queryParams.accountId }"
+                @select-material="selectMaterial"
+              />
+            </el-dialog>
           </div>
-          <!--富文本编辑器组件-->
-          <el-row>
-            <wx-editor
-              v-model="articlesAdd[isActiveAddNews].content"
-              :account-id="uploadData.accountId"
-              v-if="hackResetEditor"
-            />
-          </el-row>
+          <el-input
+            :rows="8"
+            type="textarea"
+            v-model="articlesAdd[isActiveAddNews].digest"
+            placeholder="请输入摘要"
+            class="digest"
+            maxlength="120"
+            style="float: right"
+          />
         </div>
-        <template #footer>
-          <el-button @click="dialogNewsVisible = false">取 消</el-button>
-          <el-button type="primary" @click="submitForm">提 交</el-button>
-        </template>
-      </el-dialog>
-    </Teleport>
+        <!--富文本编辑器组件-->
+        <el-row>
+          <WxEditor
+            v-model="articlesAdd[isActiveAddNews].content"
+            :account-id="uploadData.accountId"
+            v-if="hackResetEditor"
+          />
+        </el-row>
+      </div>
+      <template #footer>
+        <el-button @click="dialogNewsVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitForm">提 交</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
-<script setup name="MpDraft">
+
+<script setup lang="ts" name="MpDraft">
 import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
 import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 import { getAccessToken } from '@/utils/auth'
-import * as MpAccountApi from '@/api/mp/account'
 import * as MpDraftApi from '@/api/mp/draft'
 import * as MpFreePublishApi from '@/api/mp/freePublish'
-const message = useMessage() // 消息
+import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
 // 可以用改本地数据模拟,避免API调用超限
 // import drafts from './mock'
+const message = useMessage() // 消息
 
 const loading = ref(true) // 列表的加载中
+const list = ref<any[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   accountId: undefined
 })
-const queryFormRef = ref() // 搜索的表单
-const accountList = ref([]) // 公众号账号列表
 
 // ========== 文件上传 ==========
-const materialSelectRef = ref()
 const BASE_URL = import.meta.env.VITE_BASE_URL
-const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-permanent') // 上传永久素材的地址
-const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部
-const fileList = ref([])
-const uploadData = reactive({
+const uploadUrl = BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传永久素材的地址
+const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
+
+const materialSelectRef = ref<InstanceType<typeof WxMaterialSelect> | null>(null)
+const fileList = ref<UploadFiles>([])
+interface UploadData {
+  type: 'image' | 'video' | 'audio'
+  accountId?: number
+}
+const uploadData: UploadData = reactive({
   type: 'image',
   accountId: 1
 })
@@ -291,39 +287,28 @@ const uploadData = reactive({
 // ========== 草稿新建 or 修改 ==========
 const dialogNewsVisible = ref(false)
 const addMaterialLoading = ref(false) // 添加草稿的 loading 标识
-const articlesAdd = ref([])
+const articlesAdd = ref<any[]>([])
 const isActiveAddNews = ref(0)
 const dialogImageVisible = ref(false)
-const operateMaterial = ref('add')
+const operateMaterial = ref<'add' | 'edit'>('add')
 const articlesMediaId = ref('')
 const hackResetEditor = ref(false)
 
-/** 初始化 **/
-onMounted(async () => {
-  accountList.value = await MpAccountApi.getSimpleAccountList()
-  // 选中第一个
-  if (accountList.value.length > 0) {
-    // @ts-ignore
-    queryParams.accountId = accountList.value[0].id
-  }
-  await getList()
-})
+/** 侦听公众号变化 **/
+const onAccountChanged = (id?: number) => {
+  setAccountId(id)
+  getList()
+}
 
 // ======================== 列表查询 ========================
 /** 设置账号编号 */
-const setAccountId = (accountId) => {
-  queryParams.accountId = accountId
-  uploadData.accountId = accountId
+const setAccountId = (id?: number) => {
+  queryParams.accountId = id
+  uploadData.accountId = id
 }
 
 /** 查询列表 */
 const getList = async () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    message.error('未选中公众号,无法查询草稿箱')
-    return false
-  }
-
   loading.value = true
   try {
     const drafts = await MpDraftApi.getDraftPage(queryParams)
@@ -341,26 +326,6 @@ const getList = async () => {
   }
 }
 
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  // 默认选中第一个
-  if (queryParams.accountId) {
-    setAccountId(queryParams.accountId)
-  }
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    setAccountId(accountList.value[0].id)
-  }
-  handleQuery()
-}
-
 // ======================== 新增/修改草稿 ========================
 /** 新增按钮操作 */
 const handleAdd = () => {
@@ -372,7 +337,7 @@ const handleAdd = () => {
 }
 
 /** 更新按钮操作 */
-const handleUpdate = (item) => {
+const handleUpdate = (item: any) => {
   resetEditor()
   reset()
   articlesMediaId.value = item.mediaId
@@ -383,39 +348,30 @@ const handleUpdate = (item) => {
 }
 
 /** 提交按钮 */
-const submitForm = () => {
-  // TODO @Dhb52: 参考别的模块写法,改成 await 方式
+const submitForm = async () => {
   addMaterialLoading.value = true
-  if (operateMaterial.value === 'add') {
-    MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value)
-      .then(() => {
-        message.notifySuccess('新增成功')
-        dialogNewsVisible.value = false
-        getList()
-      })
-      .finally(() => {
-        addMaterialLoading.value = false
-      })
-  } else {
-    MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value)
-      .then(() => {
-        message.notifySuccess('更新成功')
-        dialogNewsVisible.value = false
-        getList()
-      })
-      .finally(() => {
-        addMaterialLoading.value = false
-      })
+  try {
+    if (operateMaterial.value === 'add') {
+      await MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value)
+      message.notifySuccess('新增成功')
+    } else {
+      await MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value)
+      message.notifySuccess('更新成功')
+    }
+  } finally {
+    dialogNewsVisible.value = false
+    addMaterialLoading.value = false
+    await getList()
   }
 }
 
 // 关闭弹窗
-const dialogNewsClose = async (done) => {
+const dialogNewsClose = async (onDone: () => {}) => {
   try {
     await message.confirm('修改内容可能还未保存,确定关闭吗?')
     reset()
     resetEditor()
-    done()
+    onDone()
   } catch {}
 }
 
@@ -434,7 +390,7 @@ const resetEditor = () => {
 }
 
 // 将图文向下移动
-const downNews = (index) => {
+const downNews = (index: number) => {
   let temp = articlesAdd.value[index]
   articlesAdd.value[index] = articlesAdd.value[index + 1]
   articlesAdd.value[index + 1] = temp
@@ -450,13 +406,13 @@ const upNews = (index) => {
 }
 
 // 选中指定 index 的图文
-const activeNews = (index) => {
+const activeNews = (index: number) => {
   resetEditor()
   isActiveAddNews.value = index
 }
 
 // 删除指定 index 的图文
-const minusNews = async (index) => {
+const minusNews = async (index: number) => {
   try {
     await message.confirm('确定删除该图文吗?')
     articlesAdd.value.splice(index, 1)
@@ -489,20 +445,17 @@ const buildEmptyArticle = () => {
 }
 
 // ======================== 文件上传 ========================
-const beforeThumbImageUpload = (file) => {
+const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
   addMaterialLoading.value = true
-  const isType =
-    file.type === 'image/jpeg' ||
-    file.type === 'image/png' ||
-    file.type === 'image/gif' ||
-    file.type === 'image/bmp' ||
-    file.type === 'image/jpg'
+  const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
+    rawFile.type
+  )
   if (!isType) {
     message.error('上传图片格式不对!')
     addMaterialLoading.value = false
     return false
   }
-  const isLt = file.size / 1024 / 1024 < 2
+  const isLt = rawFile.size / 1024 / 1024 < 2
   if (!isLt) {
     message.error('上传图片大小不能超过 2M!')
     addMaterialLoading.value = false
@@ -512,7 +465,7 @@ const beforeThumbImageUpload = (file) => {
   return true
 }
 
-const handleUploadSuccess = (response, file, fileList) => {
+const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
   addMaterialLoading.value = false
   if (response.code !== 0) {
     message.error('上传出错:' + response.msg)
@@ -528,7 +481,7 @@ const handleUploadSuccess = (response, file, fileList) => {
 }
 
 // 选择 or 上传完素材,设置回草稿
-const selectMaterial = (item) => {
+const selectMaterial = (item: any) => {
   dialogImageVisible.value = false
   articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId
   articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url
@@ -537,14 +490,10 @@ const selectMaterial = (item) => {
 // 打开素材选择
 const openMaterial = () => {
   dialogImageVisible.value = true
-  try {
-    materialSelectRef.value.queryParams.accountId = queryParams.accountId // 强制设置下 accountId,避免二次查询不对
-    materialSelectRef.value.handleQuery() // 刷新列表,失败也无所谓
-  } catch (e) {}
 }
 
 // ======================== 草稿箱发布 ========================
-const handlePublish = async (item) => {
+const handlePublish = async (item: any) => {
   const accountId = queryParams.accountId
   const mediaId = item.mediaId
   const content =
@@ -553,19 +502,19 @@ const handlePublish = async (item) => {
     await message.confirm(content)
     await MpFreePublishApi.submitFreePublish(accountId, mediaId)
     message.notifySuccess('发布成功')
-    await getList()
+    getList()
   } catch {}
 }
 
 /** 删除按钮操作 */
-const handleDelete = async (item) => {
+const handleDelete = async (item: any) => {
   const accountId = queryParams.accountId
   const mediaId = item.mediaId
   try {
     await message.confirm('此操作将永久删除该草稿, 是否继续?')
     await MpDraftApi.deleteDraft(accountId, mediaId)
     message.notifySuccess('删除成功')
-    await getList()
+    getList()
   } catch {}
 }
 </script>

+ 21 - 50
src/views/mp/freePublish/index.vue

@@ -11,18 +11,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" 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>
-        <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>
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -59,31 +48,37 @@
   </ContentWrap>
 </template>
 
-<script setup lang="ts" name="MpFreePublish">
+<script lang="ts" setup name="MpFreePublish">
 import * as FreePublishApi from '@/api/mp/freePublish'
-import * as MpAccountApi from '@/api/mp/account'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const list = ref<any[]>([]) // 列表的数据
+
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: undefined // 当前页数
+  accountId: undefined
 })
-const queryFormRef = ref() // 搜索的表单
-const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
+
+/** 侦听公众号变化 **/
+const onAccountChanged = (id: number | undefined) => {
+  queryParams.accountId = id
+  getList()
+}
 
 /** 查询列表 */
 const getList = async () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    message.error('未选中公众号,无法查询已发表图文')
-    return false
-  }
   try {
     loading.value = true
     const data = await FreePublishApi.getFreePublishPage(queryParams)
@@ -94,24 +89,8 @@ const getList = async () => {
   }
 }
 
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    queryParams.accountId = accountList.value[0].id
-  }
-  handleQuery()
-}
-
 /** 删除按钮操作 */
-const handleDelete = async (item) => {
+const handleDelete = async (item: any) => {
   try {
     // 删除的二次确认
     await message.delConfirm('删除后用户将无法访问此页面,确定删除?')
@@ -122,16 +101,8 @@ const handleDelete = async (item) => {
     await getList()
   } catch {}
 }
-
-onMounted(async () => {
-  accountList.value = await MpAccountApi.getSimpleAccountList()
-  // 选中第一个
-  if (accountList.value.length > 0) {
-    queryParams.accountId = accountList.value[0].id
-  }
-  await getList()
-})
 </script>
+
 <style lang="scss" scoped>
 .ope-row {
   margin-top: 5px;

+ 152 - 166
src/views/mp/material/index.vue

@@ -2,40 +2,23 @@
   <doc-alert title="公众号素材" url="https://doc.iocoder.cn/mp/material/" />
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
+    <el-form class="-mb-15px" :inline="true" label-width="68px">
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" 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>
-        <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <ContentWrap>
-    <el-tabs v-model="type" @tab-change="handleTabChange">
+    <el-tabs v-model="type" @tab-change="onTabChange">
       <!-- tab 1:图片  -->
       <el-tab-pane name="image">
         <template #label>
-          <span><Icon icon="ep:picture" />图片</span>
+          <span> <Icon icon="ep:picture" />图片 </span>
         </template>
         <div class="add_but" v-hasPermi="['mp:material:upload-permanent']">
           <el-upload
-            :action="actionUrl"
+            :action="uploadUrl"
             :headers="headers"
             multiple
             :limit="1"
@@ -58,7 +41,7 @@
               <img class="material-img" :src="item.url" />
               <div class="item-name">{{ item.name }}</div>
             </a>
-            <el-row class="ope-row" justify="center">
+            <el-row justify="center">
               <el-button
                 type="danger"
                 circle
@@ -82,11 +65,11 @@
       <!-- tab 2:语音  -->
       <el-tab-pane name="voice">
         <template #label>
-          <span><Icon icon="ep:microphone" />语音</span>
+          <span> <Icon icon="ep:microphone" />语音 </span>
         </template>
         <div class="add_but" v-hasPermi="['mp:material:upload-permanent']">
           <el-upload
-            :action="actionUrl"
+            :action="uploadUrl"
             :headers="headers"
             multiple
             :limit="1"
@@ -103,17 +86,25 @@
             </template>
           </el-upload>
         </div>
+
+        <!-- 列表 -->
         <el-table :data="list" stripe border v-loading="loading" style="margin-top: 10px">
           <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">
-              <WxVoicePlayer :url="scope.row.url" />
+              <WxVoicePlayer v-if="scope.row.url" :url="scope.row.url" />
             </template>
           </el-table-column>
-          <el-table-column label="上传时间" align="center" prop="createTime" width="180">
+          <el-table-column
+            label="上传时间"
+            align="center"
+            prop="createTime"
+            :formatter="dateFormatter"
+            width="180"
+          >
             <template #default="scope">
-              <span>{{ formatDate(scope.row.createTime) }}</span>
+              <span>{{ scope.row.createTime }}</span>
             </template>
           </el-table-column>
           <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
@@ -145,7 +136,7 @@
       <!-- tab 3:视频 -->
       <el-tab-pane name="video">
         <template #label>
-          <span><Icon icon="ep:video-play" /> 视频</span>
+          <span> <Icon icon="ep:video-play" /> 视频 </span>
         </template>
         <div class="add_but" v-hasPermi="['mp:material:upload-permanent']">
           <el-button type="primary" plain @click="handleAddVideo">新建视频</el-button>
@@ -158,7 +149,7 @@
           v-loading="addMaterialLoading"
         >
           <el-upload
-            :action="actionUrl"
+            :action="uploadUrl"
             :headers="headers"
             multiple
             :limit="1"
@@ -202,6 +193,7 @@
           </template>
         </el-dialog>
 
+        <!-- 列表 -->
         <el-table :data="list" stripe border v-loading="loading" style="margin-top: 10px">
           <el-table-column label="编号" align="center" prop="mediaId" />
           <el-table-column label="文件名" align="center" prop="name" />
@@ -209,19 +201,25 @@
           <el-table-column label="介绍" align="center" prop="introduction" />
           <el-table-column label="视频" align="center">
             <template #default="scope">
-              <WxVideoPlayer :url="scope.row.url" />
+              <WxVideoPlayer v-if="scope.row.url" :url="scope.row.url" />
             </template>
           </el-table-column>
-          <el-table-column label="上传时间" align="center" prop="createTime" width="180">
+          <el-table-column
+            label="上传时间"
+            align="center"
+            :formatter="dateFormatter"
+            prop="createTime"
+            width="180"
+          >
             <template #default="scope">
-              <span>{{ formatDate(scope.row.createTime) }}</span>
+              <span>{{ scope.row.createTime }}</span>
             </template>
           </el-table-column>
           <el-table-column label="操作" align="center" fixed="right">
             <template #default="scope">
-              <el-button type="primary" link plain @click="handleDownload(scope.row)"
-                ><Icon icon="ep:download" />下载</el-button
-              >
+              <el-button type="primary" link plain @click="handleDownload(scope.row)">
+                <Icon icon="ep:download" />下载
+              </el-button>
               <el-button
                 type="primary"
                 link
@@ -246,41 +244,66 @@
     </el-tabs>
   </ContentWrap>
 </template>
-<script setup name="MpMaterial">
+
+<script lang="ts" setup name="MpMaterial">
 import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
 import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
-import { getSimpleAccountList } from '@/api/mp/account'
-import { getMaterialPage, deletePermanentMaterial } from '@/api/mp/material'
-import { getAccessToken } from '@/utils/auth'
-import { formatDate } from '@/utils/formatTime'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import * as MpMaterialApi from '@/api/mp/material'
+import * as authUtil from '@/utils/auth'
+import { dateFormatter } from '@/utils/formatTime'
+import type {
+  FormInstance,
+  FormRules,
+  TabPaneName,
+  UploadInstance,
+  UploadProps,
+  UploadRawFile,
+  UploadUserFile
+} from 'element-plus'
 
 const BASE_URL = import.meta.env.VITE_BASE_URL
+const uploadUrl = BASE_URL + '/admin-api/mp/material/upload-permanent'
+const headers = { Authorization: 'Bearer ' + authUtil.getAccessToken() }
 
 const message = useMessage()
 
-const queryFormRef = ref()
-const uploadFormRef = ref()
-const uploadVideoRef = ref()
-
-const type = ref('image')
-// 遮罩层
-const loading = ref(false)
-// 总条数
-const total = ref(0)
-// 数据列表
-const list = ref([])
+const uploadFormRef = ref<FormInstance>()
+const uploadVideoRef = ref<UploadInstance>()
+
+const uploadRules: FormRules = {
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+  introduction: [{ required: true, message: '请输入描述', trigger: 'blur' }]
+}
+
+// 素材类型
+type MaterialType = 'image' | 'voice' | 'video'
+const type = ref<MaterialType>('image')
+const loading = ref(false) // 遮罩层
+const list = ref<any[]>([]) // 总条数
+const total = ref(0) // 数据列表
 // 查询参数
-const queryParams = reactive({
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+  permanent: boolean
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   accountId: undefined,
   permanent: true
 })
 
-const actionUrl = BASE_URL + '/admin-api/mp/material/upload-permanent'
-const headers = { Authorization: 'Bearer ' + getAccessToken() }
-const fileList = ref([])
-const uploadData = reactive({
+const fileList = ref<UploadUserFile[]>([])
+
+interface UploadData {
+  type: MaterialType
+  title: string
+  introduction: string
+}
+const uploadData: UploadData = reactive({
   type: 'image',
   title: '',
   introduction: ''
@@ -289,139 +312,93 @@ const uploadData = reactive({
 // === 视频上传,独有变量 ===
 const dialogVideoVisible = ref(false)
 const addMaterialLoading = ref(false)
-const uploadRules = reactive({
-  // 视频上传的校验规则
-  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
-  introduction: [{ required: true, message: '请输入描述', trigger: 'blur' }]
-})
-
-// 公众号账号列表
-const accountList = ref([])
-
-onMounted(() => {
-  getSimpleAccountList().then((data) => {
-    accountList.value = data
-    // 默认选中第一个
-    if (accountList.value.length > 0) {
-      setAccountId(accountList.value[0].id)
-    }
-    // 加载数据
-    getList()
-  })
-})
 
-// ======================== 列表查询 ========================
-/** 设置账号编号 */
-const setAccountId = (accountId) => {
-  queryParams.accountId = accountId
-  uploadData.accountId = accountId
+/** 侦听公众号变化 **/
+const onAccountChanged = (id?: number) => {
+  queryParams.accountId = id
+  getList()
 }
 
+// ======================== 列表查询 ========================
 /** 查询列表 */
-const getList = () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    message.error('未选中公众号,无法查询草稿箱')
-    return false
-  }
-
+const getList = async () => {
   loading.value = true
-  getMaterialPage({
-    ...queryParams,
-    type: type.value
-  })
-    .then((data) => {
-      list.value = data.list
-      total.value = data.total
-    })
-    .finally(() => {
-      loading.value = false
+  try {
+    const data = await MpMaterialApi.getMaterialPage({
+      ...queryParams,
+      type: type.value
     })
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
 }
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
-  // 默认选中第一个
-  if (queryParams.accountId) {
-    setAccountId(queryParams.accountId)
-  }
   getList()
 }
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    setAccountId(accountList.value[0].id)
-  }
-  handleQuery()
-}
-
-const handleTabChange = (tabName) => {
+const onTabChange = (tabName: TabPaneName) => {
   // 设置 type
-  uploadData.type = tabName
+  uploadData.type = tabName as MaterialType
+
+  // 提前情况数据,避免tab切换后显示垃圾数据
+  list.value = []
+  total.value = 0
+
   // 从第一页开始查询
   handleQuery()
 }
 
 // ======================== 文件上传 ========================
-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
+const beforeUpload = (rawFile: UploadRawFile, category: 'image' | 'audio' | 'video'): boolean => {
+  let allowTypes: string[] = []
+  let maxSizeMB = 0
+  let name = ''
+
+  switch (category) {
+    case 'image':
+      allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
+      maxSizeMB = 2
+      name = '图片'
+      break
+    case 'audio':
+      allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr']
+      maxSizeMB = 2
+      name = '图片'
+      break
+    case 'video':
+      allowTypes = ['video/mp4']
+      maxSizeMB = 10
+      name = '视频'
   }
-  loading.value = true
-  return true
-}
 
-const beforeVoiceUpload = (file) => {
-  const isType =
-    file.type === 'audio/mp3' ||
-    file.type === 'audio/wma' ||
-    file.type === 'audio/wav' ||
-    file.type === 'audio/amr'
-  const isLt = file.size / 1024 / 1024 < 2
-  if (!isType) {
-    message.error('上传语音格式不对!')
+  if (!allowTypes.includes(rawFile.type)) {
+    message.error(`上传${name}格式不对!`)
     return false
   }
-  if (!isLt) {
-    message.error('上传语音大小不能超过 2M!')
+  // 校验大小
+  if (rawFile.size / 1024 / 1024 > maxSizeMB) {
+    message.error(`上传${name}大小不能超过${maxSizeMB}M!`)
     return false
   }
   loading.value = true
   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
-  }
-  addMaterialLoading.value = true
-  return true
-}
+const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+  beforeUpload(rawFile, 'image')
+
+const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+  beforeUpload(rawFile, 'audio')
 
-const handleUploadSuccess = (response, file, fileList) => {
+const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+  beforeUpload(rawFile, 'video')
+
+const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
   loading.value = false
   addMaterialLoading.value = false
   if (response.code !== 0) {
@@ -440,20 +417,21 @@ const handleUploadSuccess = (response, file, fileList) => {
 }
 
 // 下载文件
-const handleDownload = (row) => {
+const handleDownload = (row: any) => {
   window.open(row.url, '_blank')
 }
 
 // 提交 video 新建的表单
 const submitVideo = () => {
-  uploadFormRef.value.validate((valid) => {
+  uploadFormRef.value?.validate((valid) => {
     if (!valid) {
       return false
     }
-    uploadVideoRef.value.submit()
+    uploadVideoRef.value?.submit()
   })
 }
 
+// 弹出 video 新建的表单
 const handleAddVideo = () => {
   resetVideo()
   dialogVideoVisible.value = true
@@ -474,9 +452,9 @@ const resetVideo = () => {
 }
 
 // ======================== 其它操作 ========================
-const handleDelete = async (item) => {
+const handleDelete = async (item: any) => {
   await message.confirm('此操作将永久删除该文件, 是否继续?')
-  await deletePermanentMaterial(item.id)
+  await MpMaterialApi.deletePermanentMaterial(item.id)
   message.alertSuccess('删除成功')
 }
 </script>
@@ -487,40 +465,48 @@ const handleDelete = async (item) => {
   width: 100%;
   column-gap: 10px;
   column-count: 5;
-  margin-top: 10px; /* 芋道源码:增加 10px,避免顶着上面 */
+  margin-top: 10px;
+  /* 芋道源码:增加 10px,避免顶着上面 */
 }
+
 .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>

+ 49 - 90
src/views/mp/menu/index.vue

@@ -4,18 +4,7 @@
   <ContentWrap>
     <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="accountId" placeholder="请选择公众号" 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>
-        <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -26,7 +15,7 @@
       <!--左边配置菜单-->
       <div class="left">
         <div class="weixin-hd">
-          <div class="weixin-title">{{ name }}</div>
+          <div class="weixin-title">{{ accountName }}</div>
         </div>
         <div class="weixin-menu menu_main clearfix">
           <div class="menu_bottom" v-for="(item, i) of menuList" :key="i">
@@ -82,7 +71,7 @@
       <div v-if="showRightFlag" class="right">
         <div class="configure_page">
           <div class="delete_btn">
-            <el-button size="small" type="danger" @click="handleDeleteMenu(tempObj)">
+            <el-button size="small" type="danger" @click="handleDeleteMenu">
               删除当前菜单<Icon icon="ep:delete" />
             </el-button>
           </div>
@@ -169,7 +158,7 @@
                 <div v-else>
                   <el-row justify="center">
                     <el-col :span="24" style="text-align: center">
-                      <el-button type="success" @click="openMaterial">
+                      <el-button type="success" @click="dialogNewsVisible = true">
                         素材库选择<Icon icon="ep:circle-check" />
                       </el-button>
                     </el-col>
@@ -199,26 +188,26 @@
     </div>
   </ContentWrap>
 </template>
-<script setup name="MpMenu">
-import { handleTree } from '@/utils/tree'
+<script lang="ts" setup name="MpMenu">
 import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
 import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
-import { deleteMenu, getMenuList, saveMenu } from '@/api/mp/menu'
-import * as MpAccountApi from '@/api/mp/account'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import * as MpMenuApi from '@/api/mp/menu'
+import { handleTree } from '@/utils/tree'
 import menuOptions from './menuOptions'
+
 const message = useMessage() // 消息
 
 // ======================== 列表查询 ========================
-const loading = ref(true) // 遮罩层
-const accountId = ref(undefined) // 公众号Id
-const name = ref('') // 公众号名
-const menuList = ref({ children: [] })
-const accountList = ref([]) // 公众号账号列表
+const loading = ref(false) // 遮罩层
+const accountId = ref<number | undefined>()
+const accountName = ref<string | undefined>('')
+const menuList = ref<any>({ children: [] })
 
 // ======================== 菜单操作 ========================
 const isActive = ref(-1) // 一级菜单点中样式
-const isSubMenuActive = ref(-1) // 一级菜单点中样式
+const isSubMenuActive = ref<string | number>(-1) // 一级菜单点中样式
 const isSubMenuFlag = ref(-1) // 二级菜单显示标志
 
 // ======================== 菜单编辑 ========================
@@ -226,67 +215,42 @@ const showRightFlag = ref(false) // 右边配置显示默认详情还是配置
 const nameMaxLength = ref(0) // 菜单名称最大长度;1 级是 4 字符;2 级是 7 字符;
 const showConfigureContent = ref(true) // 是否展示配置内容;如果有子菜单,就不显示配置内容
 const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件
-const tempObj = ref({}) // 右边临时变量,作为中间值牵引关系
+const tempObj = ref<any>({}) // 右边临时变量,作为中间值牵引关系
 
-const tempSelfObj = ref({
-  // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数
-})
+// 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数
+const tempSelfObj = ref<any>({})
 const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗
 
-onMounted(async () => {
-  accountList.value = await MpAccountApi.getSimpleAccountList()
-  // 选中第一个
-  if (accountList.value.length > 0) {
-    // @ts-ignore
-    setAccountId(accountList.value[0].id)
-  }
-  await getList()
-})
-
-// ======================== 列表查询 ========================
-/** 设置账号编号 */
-const setAccountId = (id) => {
+/** 侦听公众号变化 **/
+const onAccountChanged = (id?: number, name?: string) => {
   accountId.value = id
-  name.value = accountList.value.find((item) => item.id === accountId.value)?.name
+  accountName.value = name
+  getList()
 }
 
+/** 查询并转换菜单 **/
 const getList = async () => {
   loading.value = false
-  getMenuList(accountId.value)
-    .then((response) => {
-      const menuData = convertMenuList(response)
-      menuList.value = handleTree(menuData, 'id')
-    })
-    .finally(() => {
-      loading.value = false
-    })
+  try {
+    const data = await MpMenuApi.getMenuList(accountId.value)
+    const menuData = convertMenuList(data)
+    menuList.value = handleTree(menuData, 'id')
+  } finally {
+    loading.value = false
+  }
 }
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
   resetForm()
-  // 默认选中第一个
-  if (accountId.value) {
-    setAccountId(accountId.value)
-  }
   getList()
 }
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  resetForm()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    setAccountId(accountList.value[0].id)
-  }
-  handleQuery()
-}
-
 // 将后端返回的 menuList,转换成前端的 menuList
-const convertMenuList = (list) => {
+const convertMenuList = (list: any[]) => {
   if (!list) return []
 
-  const menuList = []
+  const result: any[] = []
   list.forEach((item) => {
     const menu = {
       ...item
@@ -313,9 +277,9 @@ const convertMenuList = (list) => {
         hqMusicUrl: item.replyHqMusicUrl
       }
     }
-    menuList.push(menu)
+    result.push(menu)
   })
-  return menuList
+  return result
 }
 
 // 重置表单,清空表单数据
@@ -328,7 +292,7 @@ const resetForm = () => {
   // 菜单编辑
   showRightFlag.value = false
   nameMaxLength.value = 0
-  showConfigureContent.value = 0
+  showConfigureContent.value = false
   hackResetWxReplySelect.value = false
   tempObj.value = {}
   tempSelfObj.value = {}
@@ -337,7 +301,7 @@ const resetForm = () => {
 
 // ======================== 菜单操作 ========================
 // 一级菜单点击事件
-const menuClick = (i, item) => {
+const menuClick = (i: number, item: any) => {
   // 右侧的表单相关
   resetEditor()
   showRightFlag.value = true // 右边菜单
@@ -354,11 +318,10 @@ const menuClick = (i, item) => {
 }
 
 // 二级菜单点击事件
-const subMenuClick = (subItem, index, k) => {
+const subMenuClick = (subItem: any, index: number, k: number) => {
   // 右侧的表单相关
   resetEditor()
   showRightFlag.value = true // 右边菜单
-  console.log(subItem)
   tempObj.value = subItem // 将点击的数据放到临时变量,对象有引用作用
   tempSelfObj.value.grand = '2' // 表示二级菜单
   tempSelfObj.value.index = index // 表示一级菜单索引
@@ -373,7 +336,7 @@ const subMenuClick = (subItem, index, k) => {
 
 // 添加横向一级菜单
 const addMenu = () => {
-  const menuKeyLength = menuList.value.length
+  const menuKeyLength: number = menuList.value.length
   const addButton = {
     name: '菜单名称',
     children: [],
@@ -384,10 +347,10 @@ const addMenu = () => {
     }
   }
   menuList.value[menuKeyLength] = addButton
-  menuClick(menuKeyLength.value - 1, addButton)
+  menuClick(menuKeyLength - 1, addButton)
 }
 // 添加横向二级菜单;item 表示要操作的父菜单
-const addSubMenu = (i, item) => {
+const addSubMenu = (i: number, item: any) => {
   // 清空父菜单的属性,因为它只需要 name 属性即可
   if (!item.children || item.children.length <= 0) {
     item.children = []
@@ -403,8 +366,8 @@ const addSubMenu = (i, item) => {
     showConfigureContent.value = false
   }
 
-  let subMenuKeyLength = item.children.length // 获取二级菜单key长度
-  let addButton = {
+  const subMenuKeyLength = item.children.length // 获取二级菜单key长度
+  const addButton = {
     name: '子菜单名称',
     reply: {
       // 用于存储回复内容
@@ -441,9 +404,9 @@ const handleDeleteMenu = async () => {
 // ======================== 菜单编辑 ========================
 const handleSave = async () => {
   try {
-    await message.confirm('确定要删除吗?')
+    await message.confirm('确定要保存吗?')
     loading.value = true
-    await saveMenu(accountId.value, convertMenuFormList())
+    await MpMenuApi.saveMenu(accountId.value, convertMenuFormList())
     getList()
     message.notifySuccess('发布成功')
   } finally {
@@ -455,7 +418,6 @@ const handleSave = async () => {
 const resetEditor = () => {
   hackResetWxReplySelect.value = false // 销毁组件
   nextTick(() => {
-    console.log('nextTick')
     hackResetWxReplySelect.value = true // 重建组件
   })
 }
@@ -464,7 +426,7 @@ const handleDelete = async () => {
   try {
     await message.confirm('确定要删除吗?')
     loading.value = true
-    await deleteMenu(accountId.value)
+    await MpMenuApi.deleteMenu(accountId.value)
     handleQuery()
     message.notifySuccess('清空成功')
   } finally {
@@ -474,9 +436,9 @@ const handleDelete = async () => {
 
 // 将前端的 menuList,转换成后端接收的 menuList
 const convertMenuFormList = () => {
-  const result = []
+  const result: any[] = []
   menuList.value.forEach((item) => {
-    let menu = convertMenuForm(item)
+    const menu = convertMenuForm(item)
     result.push(menu)
 
     // 处理子菜单
@@ -492,7 +454,7 @@ const convertMenuFormList = () => {
 }
 
 // 将前端的 menu,转换成后端接收的 menu
-const convertMenuForm = (menu) => {
+const convertMenuForm = (menu: any) => {
   let result = {
     ...menu,
     children: undefined, // 不处理子节点
@@ -515,11 +477,7 @@ const convertMenuForm = (menu) => {
 }
 
 // ======================== 菜单编辑(素材选择) ========================
-const openMaterial = () => {
-  dialogNewsVisible.value = true
-}
-
-const selectMaterial = (item) => {
+const selectMaterial = (item: any) => {
   const articleId = item.articleId
   const articles = item.content.newsItem
   // 提示,针对多图文
@@ -546,6 +504,7 @@ const deleteMaterial = () => {
   delete tempObj.value['replyArticles']
 }
 </script>
+
 <!--本组件样式-->
 <style lang="scss" scoped="scoped">
 /* 公共颜色变量 */

+ 67 - 73
src/views/mp/message/index.vue

@@ -9,14 +9,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
-          <el-option
-            v-for="item in accountList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item label="消息类型" prop="type">
         <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px">
@@ -84,70 +77,76 @@
       <el-table-column label="内容" prop="content">
         <template #default="scope">
           <!-- 【事件】区域 -->
-          <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'">
+          <div v-if="scope.row.type === MsgType.Event && scope.row.event === 'subscribe'">
             <el-tag type="success">关注</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'unsubscribe'">
             <el-tag type="danger">取消关注</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'CLICK'">
             <el-tag>点击菜单</el-tag>
             【{{ scope.row.eventKey }}】
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'VIEW'">
             <el-tag>点击菜单链接</el-tag>
             【{{ scope.row.eventKey }}】
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'">
+          <div
+            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_waitmsg'"
+          >
             <el-tag>扫码结果</el-tag>
             【{{ scope.row.eventKey }}】
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_push'">
             <el-tag>扫码结果</el-tag>
             【{{ scope.row.eventKey }}】
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_sysphoto'">
             <el-tag>系统拍照发图</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'">
+          <div
+            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_photo_or_album'"
+          >
             <el-tag>拍照或者相册</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'">
+          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_weixin'">
             <el-tag>微信相册</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'">
+          <div
+            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'location_select'"
+          >
             <el-tag>选择地理位置</el-tag>
           </div>
-          <div v-else-if="scope.row.type === 'event'">
+          <div v-else-if="scope.row.type === MsgType.Event">
             <el-tag type="danger">未知事件类型</el-tag>
           </div>
           <!-- 【消息】区域 -->
-          <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div>
-          <div v-else-if="scope.row.type === 'voice'">
+          <div v-else-if="scope.row.type === MsgType.Text">{{ scope.row.content }}</div>
+          <div v-else-if="scope.row.type === MsgType.Voice">
             <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" />
           </div>
-          <div v-else-if="scope.row.type === 'image'">
+          <div v-else-if="scope.row.type === MsgType.Image">
             <a target="_blank" :href="scope.row.mediaUrl">
               <img :src="scope.row.mediaUrl" style="width: 100px" />
             </a>
           </div>
-          <div v-else-if="scope.row.type === 'video' || scope.row.type === 'shortvideo'">
+          <div v-else-if="scope.row.type === MsgType.Video || scope.row.type === 'shortvideo'">
             <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
           </div>
-          <div v-else-if="scope.row.type === 'link'">
+          <div v-else-if="scope.row.type === MsgType.Link">
             <el-tag>链接</el-tag>
             <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
           </div>
-          <div v-else-if="scope.row.type === 'location'">
-            <wx-location
+          <div v-else-if="scope.row.type === MsgType.Location">
+            <WxLocation
               :label="scope.row.label"
               :location-y="scope.row.locationY"
               :location-x="scope.row.locationX"
             />
           </div>
-          <div v-else-if="scope.row.type === 'music'">
-            <wx-music
+          <div v-else-if="scope.row.type === MsgType.Music">
+            <WxMusic
               :title="scope.row.title"
               :description="scope.row.description"
               :thumb-media-url="scope.row.thumbMediaUrl"
@@ -155,8 +154,8 @@
               :hq-music-url="scope.row.hqMusicUrl"
             />
           </div>
-          <div v-else-if="scope.row.type === 'news'">
-            <wx-news :articles="scope.row.articles" />
+          <div v-else-if="scope.row.type === MsgType.News">
+            <WxNews :articles="scope.row.articles" />
           </div>
           <div v-else>
             <el-tag type="danger">未知消息类型</el-tag>
@@ -177,7 +176,7 @@
       </el-table-column>
     </el-table>
     <!-- 分页组件 -->
-    <pagination
+    <Pagination
       v-show="total > 0"
       :total="total"
       v-model:page="queryParams.pageNo"
@@ -186,9 +185,14 @@
     />
 
     <!-- 发送消息的弹窗 -->
-    <el-dialog title="粉丝消息列表" v-model="open" @click="openDialog()" width="50%">
+    <el-dialog
+      title="粉丝消息列表"
+      v-model="showMessageBox"
+      @click="showMessageBox = true"
+      width="50%"
+    >
       <template #footer>
-        <wx-msg :user-id="userId" v-if="open" />
+        <WxMsg :user-id="userId" v-if="showMessageBox" />
       </template>
     </el-dialog>
   </ContentWrap>
@@ -200,17 +204,27 @@ 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 * as MpAccountApi from '@/api/mp/account'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 import * as MpMessageApi from '@/api/mp/message'
-
-const message = useMessage() // 消息弹窗
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
+import { MsgType } from '@/views/mp/components/wx-msg/types'
+import type { FormInstance } from 'element-plus'
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const list = ref<any[]>([]) // 列表的数据
+
+// 搜索参数
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  openid: string | null
+  accountId: number | null
+  type: MsgType | null
+  createTime: string[] | []
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   openid: null,
@@ -218,19 +232,18 @@ const queryParams = reactive({
   type: null,
   createTime: []
 })
-const queryFormRef = ref() // 搜索的表单
-// TODO 芋艿:下面应该移除
-const open = ref(false) // 是否显示弹出层
+const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
+const showMessageBox = ref(false) // 是否显示弹出层
 const userId = ref(0) // 操作的用户编号
-const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
+
+/** 侦听accountId */
+const onAccountChanged = (id?: number) => {
+  queryParams.accountId = id as number
+  handleQuery()
+}
 
 /** 查询列表 */
 const getList = async () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    await message.error('未选中公众号,无法查询消息')
-    return
-  }
   try {
     loading.value = true
     const data = await MpMessageApi.getMessagePage(queryParams)
@@ -249,34 +262,15 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = async () => {
-  queryFormRef.value.resetFields()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    // @ts-ignore
-    queryParams.accountId = accountList.value[0].id
-  }
+  const accountId = queryParams.accountId
+  queryFormRef.value?.resetFields()
+  queryParams.accountId = accountId
   handleQuery()
 }
-const handleSend = async (row) => {
-  userId.value = row.userId
-  open.value = true
-}
 
-const openDialog = () => {
-  open.value = true
+/** 打开消息发送窗口 */
+const handleSend = async (row: any) => {
+  userId.value = row.userId
+  showMessageBox.value = true
 }
-// const closeDiaLog = () => {
-//   open.value = false
-// }
-
-/** 初始化 **/
-onMounted(async () => {
-  accountList.value = await MpAccountApi.getSimpleAccountList()
-  // 选中第一个
-  if (accountList.value.length > 0) {
-    // @ts-ignore
-    queryParams.accountId = accountList.value[0].id
-  }
-  await getList()
-})
 </script>

+ 12 - 7
src/views/mp/tag/TagForm.vue

@@ -19,24 +19,30 @@
 </template>
 <script setup lang="ts">
 import * as MpTagApi from '@/api/mp/tag'
+import type { FormInstance, FormRules } from 'element-plus'
+
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formType = ref<'create' | 'update' | ''>('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   accountId: -1,
   name: ''
 })
-const formRules = reactive({
+const formRules: FormRules = {
   name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
+}
+const formRef = ref<FormInstance | null>(null) // 表单 Ref
+
+const emit = defineEmits<{
+  (e: 'success'): void
+}>()
 
 /** 打开弹窗 */
-const open = async (type: string, accountId: number, id?: number) => {
+const open = async (type: 'create' | 'update', accountId: number, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
@@ -55,11 +61,10 @@ const open = async (type: string, accountId: number, id?: number) => {
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
   // 校验表单
   if (!formRef) return
-  const valid = await formRef.value.validate()
+  const valid = await formRef.value?.validate()
   if (!valid) return
   // 提交请求
   formLoading.value = true

+ 21 - 63
src/views/mp/tag/index.vue

@@ -11,29 +11,9 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" 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="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入标签名称"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item>
-        <el-form-item>
-          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        </el-form-item>
         <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
@@ -93,30 +73,36 @@
 <script setup lang="ts" name="MpTag">
 import { dateFormatter } from '@/utils/formatTime'
 import * as MpTagApi from '@/api/mp/tag'
-import * as MpAccountApi from '@/api/mp/account'
 import TagForm from './TagForm.vue'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const list = ref<any>([]) // 列表的数据
+
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: undefined,
-  name: null
+  accountId: undefined
 })
-const queryFormRef = ref() // 搜索的表单
-const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
+const formRef = ref<InstanceType<typeof TagForm> | null>(null)
+
+/** 侦听公众号变化 **/
+const onAccountChanged = (id?: number) => {
+  queryParams.pageNo = 1
+  queryParams.accountId = id
+  getList()
+}
 
 /** 查询列表 */
 const getList = async () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    await message.error('未选中公众号,无法查询标签')
-    return
-  }
   try {
     loading.value = true
     const data = await MpTagApi.getTagPage(queryParams)
@@ -127,26 +113,9 @@ const getList = async () => {
   }
 }
 
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    queryParams.accountId = accountList.value[0].id
-  }
-  handleQuery()
-}
-
 /** 添加/修改操作 */
-const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, queryParams.accountId, id)
+  formRef.value?.open(type, queryParams.accountId as number, id)
 }
 
 /** 删除按钮操作 */
@@ -166,20 +135,9 @@ const handleDelete = async (id: number) => {
 const handleSync = async () => {
   try {
     await message.confirm('是否确认同步标签?')
-    // @ts-ignore
-    await MpTagApi.syncTag(queryParams.accountId)
+    await MpTagApi.syncTag(queryParams.accountId as number)
     message.success('同步标签成功')
     await getList()
   } catch {}
 }
-
-/** 初始化 **/
-onMounted(async () => {
-  accountList.value = await MpAccountApi.getSimpleAccountList()
-  // 选中第一个
-  if (accountList.value.length > 0) {
-    queryParams.accountId = accountList.value[0].id
-  }
-  await getList()
-})
 </script>

+ 32 - 39
src/views/mp/user/index.vue

@@ -11,14 +11,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
-          <el-option
-            v-for="item in accountList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
+        <WxMpSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item label="用户标识" prop="openid">
         <el-input
@@ -39,8 +32,8 @@
         />
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+        <el-button @click="handleQuery"> <Icon icon="ep:search" />搜索 </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" />重置 </el-button>
         <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']">
           <Icon icon="ep:refresh" class="mr-5px" /> 同步
         </el-button>
@@ -102,33 +95,44 @@
 </template>
 <script lang="ts" setup name="MpUser">
 import { dateFormatter } from '@/utils/formatTime'
-import * as MpAccountApi from '@/api/mp/account'
 import * as MpUserApi from '@/api/mp/user'
 import * as MpTagApi from '@/api/mp/tag'
+import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import type { FormInstance } from 'element-plus'
 import UserForm from './UserForm.vue'
+
 const message = useMessage() // 消息
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const list = ref<any[]>([]) // 列表的数据
+
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  accountId?: number
+  openid: string | null
+  nickname: string | null
+}
+const queryParams: QueryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  accountId: null,
+  accountId: undefined,
   openid: null,
   nickname: null
 })
-const queryFormRef = ref() // 搜索的表单
-const accountList = ref([]) // 公众号账号列表
-const tagList = ref([]) // 公众号标签列表
+const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
+const tagList = ref<any[]>([]) // 公众号标签列表
+
+/** 侦听公众号变化 **/
+const onAccountChanged = (id?: number) => {
+  queryParams.pageNo = 1
+  queryParams.accountId = id
+  getList()
+}
 
 /** 查询列表 */
 const getList = async () => {
-  // 如果没有选中公众号账号,则进行提示。
-  if (!queryParams.accountId) {
-    message.error('未选中公众号,无法查询用户')
-    return false
-  }
   try {
     loading.value = true
     const data = await MpUserApi.getUserPage(queryParams)
@@ -147,26 +151,23 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  // 默认选中第一个
-  if (accountList.value.length > 0) {
-    queryParams.accountId = accountList.value[0].id
-  }
+  const accountId = queryParams.accountId
+  queryFormRef.value?.resetFields()
+  queryParams.accountId = accountId
   handleQuery()
 }
 
 /** 添加/修改操作 */
-const formRef = ref()
+const formRef = ref<InstanceType<typeof UserForm> | null>(null)
 const openForm = (id: number) => {
-  formRef.value.open(id)
+  formRef.value?.open(id)
 }
 
 /** 同步标签 */
 const handleSync = async () => {
-  const accountId = queryParams.accountId
   try {
     await message.confirm('是否确认同步粉丝?')
-    await MpUserApi.syncUser(accountId)
+    await MpUserApi.syncUser(queryParams.accountId)
     message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询')
     await getList()
   } catch {}
@@ -174,14 +175,6 @@ const handleSync = async () => {
 
 /** 初始化 */
 onMounted(async () => {
-  // 加载标签
   tagList.value = await MpTagApi.getSimpleTagList()
-
-  // 加载账号
-  accountList.value = await MpAccountApi.getSimpleAccountList()
-  if (accountList.value.length > 0) {
-    queryParams.accountId = accountList.value[0].id
-  }
-  await getList()
 })
 </script>

+ 142 - 0
src/views/pay/app/AppForm.vue

@@ -0,0 +1,142 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="160px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="应用名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入应用名" />
+      </el-form-item>
+      <el-form-item label="所属商户" prop="merchantId">
+        <el-select v-model="formData.merchantId" placeholder="请选择所属商户">
+          <el-option
+            v-for="item in merchantList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="开启状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="支付结果的回调地址" prop="payNotifyUrl">
+        <el-input v-model="formData.payNotifyUrl" placeholder="请输入支付结果的回调地址" />
+      </el-form-item>
+      <el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
+        <el-input v-model="formData.refundNotifyUrl" placeholder="请输入退款结果的回调地址" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as AppApi from '@/api/pay/app'
+import * as MerchantApi from '@/api/pay/merchant'
+import { CommonStatusEnum } from '@/utils/constants'
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  packageId: undefined,
+  contactName: undefined,
+  contactMobile: undefined,
+  accountCount: undefined,
+  expireTime: undefined,
+  domain: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }],
+  payNotifyUrl: [{ required: true, message: '支付结果的回调地址不能为空', trigger: 'blur' }],
+  refundNotifyUrl: [{ required: true, message: '退款结果的回调地址不能为空', trigger: 'blur' }],
+  merchantId: [{ required: true, message: '商户编号不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const merchantList = ref([]) // 商户列表
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await AppApi.getApp(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 加载商户列表
+  merchantList.value = await MerchantApi.getMerchantListByName()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as AppApi.AppVO
+    if (formType.value === 'create') {
+      await AppApi.createApp(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await AppApi.updateApp(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    status: CommonStatusEnum.ENABLE,
+    remark: undefined,
+    payNotifyUrl: undefined,
+    refundNotifyUrl: undefined,
+    merchantId: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 0 - 71
src/views/pay/app/app.data.ts

@@ -1,71 +0,0 @@
-import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
-const { t } = useI18n() // 国际化
-
-// 表单校验
-export const rules = reactive({
-  name: [required],
-  status: [required],
-  payNotifyUrl: [required],
-  refundNotifyUrl: [required],
-  merchantId: [required]
-})
-
-// CrudSchema
-const crudSchemas = reactive<VxeCrudSchema>({
-  primaryKey: 'id',
-  primaryType: 'seq',
-  primaryTitle: '编号',
-  action: true,
-  columns: [
-    {
-      title: '应用名',
-      field: 'name',
-      isSearch: true
-    },
-    {
-      title: '商户名称',
-      field: 'payMerchant',
-      isSearch: true
-    },
-    {
-      title: t('common.status'),
-      field: 'status',
-      dictType: DICT_TYPE.COMMON_STATUS,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: '支付结果的回调地址',
-      field: 'payNotifyUrl',
-      isSearch: true
-    },
-    {
-      title: '退款结果的回调地址',
-      field: 'refundNotifyUrl',
-      isSearch: true
-    },
-    {
-      title: '商户名称',
-      field: 'merchantName',
-      isSearch: true
-    },
-    {
-      title: '备注',
-      field: 'remark',
-      isTable: false,
-      isSearch: true
-    },
-    {
-      title: t('common.createTime'),
-      field: 'createTime',
-      isForm: false,
-      search: {
-        show: true,
-        itemRender: {
-          name: 'XDataTimePicker'
-        }
-      }
-    }
-  ]
-})
-export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

+ 437 - 129
src/views/pay/app/index.vue

@@ -1,155 +1,463 @@
 <template>
+  <!-- 搜索 -->
   <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <template #toolbar_buttons>
-        <!-- 操作:新增 -->
-        <XButton
-          type="primary"
-          preIcon="ep:zoom-in"
-          :title="t('action.add')"
-          v-hasPermi="['pay:app:create']"
-          @click="handleCreate()"
-        />
-        <!-- 操作:导出 -->
-        <XButton
-          type="warning"
-          preIcon="ep:download"
-          :title="t('action.export')"
-          v-hasPermi="['pay:app:export']"
-          @click="exportList('应用信息.xls')"
-        />
-      </template>
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:修改 -->
-        <XTextButton
-          preIcon="ep:edit"
-          :title="t('action.edit')"
-          v-hasPermi="['pay:app:update']"
-          @click="handleUpdate(row.id)"
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="应用名" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入应用名"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
         />
-        <!-- 操作:详情 -->
-        <XTextButton
-          preIcon="ep:view"
-          :title="t('action.detail')"
-          v-hasPermi="['pay:app:query']"
-          @click="handleDetail(row.id)"
+      </el-form-item>
+      <el-form-item label="商户名称" prop="contactName">
+        <el-input
+          v-model="queryParams.contactName"
+          placeholder="请输入商户名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
         />
-        <!-- 操作:删除 -->
-        <XTextButton
-          preIcon="ep:delete"
-          :title="t('action.del')"
-          v-hasPermi="['pay:app:delete']"
-          @click="deleteData(row.id)"
+      </el-form-item>
+      <el-form-item label="开启状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择开启状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :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>
+      </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"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['system:tenant:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['system:tenant:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
 
-  <XModal v-model="dialogVisible" :title="dialogTitle">
-    <!-- 对话框(添加 / 修改) -->
-    <Form
-      v-if="['create', 'update'].includes(actionType)"
-      :schema="allSchemas.formSchema"
-      :rules="rules"
-      ref="formRef"
-    />
-    <!-- 对话框(详情) -->
-    <Descriptions
-      v-if="actionType === 'detail'"
-      :schema="allSchemas.detailSchema"
-      :data="detailData"
-    />
-    <!-- 操作按钮 -->
-    <template #footer>
-      <!-- 按钮:保存 -->
-      <XButton
-        v-if="['create', 'update'].includes(actionType)"
-        type="primary"
-        :title="t('action.save')"
-        :loading="actionLoading"
-        @click="submitForm()"
+  <!-- 列表 -->
+  <ContentWrap>
+    <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.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="商户名称" align="center" prop="payMerchant.name" />
+      <el-table-column label="支付宝配置" align="center">
+        <el-table-column :label="PayChannelEnum.ALIPAY_APP.name" align="center">
+          <template #default="scope">
+            <el-button
+              type="success"
+              v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.ALIPAY_APP.code)"
+              @click="
+                handleUpdateChannel(scope.row, PayChannelEnum.ALIPAY_APP.code, PayType.ALIPAY)
+              "
+              circle
+            >
+              <Icon icon="ep:check" />
+            </el-button>
+            <el-button
+              v-else
+              type="danger"
+              circle
+              @click="
+                handleCreateChannel(scope.row, PayChannelEnum.ALIPAY_APP.code, PayType.ALIPAY)
+              "
+            >
+              <Icon icon="ep:close" />
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column :label="PayChannelEnum.ALIPAY_PC.name" align="center">
+          <template #default="scope">
+            <el-button
+              type="success"
+              circle
+              v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.ALIPAY_PC.code)"
+              @click="handleUpdateChannel(scope.row, PayChannelEnum.ALIPAY_PC.code, PayType.ALIPAY)"
+            >
+              <Icon icon="ep:check" />
+            </el-button>
+            <el-button
+              v-else
+              type="danger"
+              circle
+              @click="handleCreateChannel(scope.row, PayChannelEnum.ALIPAY_PC.code, PayType.ALIPAY)"
+            >
+              <Icon icon="ep:close" />
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column :label="PayChannelEnum.ALIPAY_WAP.name" align="center">
+          <template #default="scope">
+            <el-button
+              type="success"
+              circle
+              v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.ALIPAY_WAP.code)"
+              @click="
+                handleUpdateChannel(scope.row, PayChannelEnum.ALIPAY_WAP.code, PayType.ALIPAY)
+              "
+            >
+              <Icon icon="ep:check" />
+            </el-button>
+            <el-button
+              v-else
+              type="danger"
+              circle
+              @click="
+                handleCreateChannel(scope.row, PayChannelEnum.ALIPAY_WAP.code, PayType.ALIPAY)
+              "
+            >
+              <Icon icon="ep:close" />
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column :label="PayChannelEnum.ALIPAY_QR.name" align="center">
+          <template #default="scope">
+            <el-button
+              type="success"
+              circle
+              v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.ALIPAY_QR.code)"
+              @click="handleUpdateChannel(scope.row, PayChannelEnum.ALIPAY_QR.code, PayType.ALIPAY)"
+            >
+              <Icon icon="ep:check" />
+            </el-button>
+            <el-button
+              v-else
+              type="danger"
+              circle
+              @click="handleCreateChannel(scope.row, PayChannelEnum.ALIPAY_QR.code, PayType.ALIPAY)"
+            >
+              <Icon icon="ep:close" />
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column :label="PayChannelEnum.ALIPAY_BAR.name" align="center">
+          <template #default="scope">
+            <el-button
+              type="success"
+              circle
+              v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.ALIPAY_BAR.code)"
+              @click="
+                handleUpdateChannel(scope.row, PayChannelEnum.ALIPAY_BAR.code, PayType.ALIPAY)
+              "
+            >
+              <Icon icon="ep:check" />
+            </el-button>
+            <el-button
+              v-else
+              type="danger"
+              circle
+              @click="
+                handleCreateChannel(scope.row, PayChannelEnum.ALIPAY_BAR.code, PayType.ALIPAY)
+              "
+            >
+              <Icon icon="ep:close" />
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table-column>
+      <el-table-column label="微信配置" align="center">
+        <el-table-column :label="PayChannelEnum.WX_LITE.name" align="center">
+          <template #default="scope">
+            <el-button
+              type="success"
+              circle
+              v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.WX_LITE.code)"
+              @click="handleUpdateChannel(scope.row, PayChannelEnum.WX_LITE.code, PayType.WECHAT)"
+            >
+              <Icon icon="ep:check" />
+            </el-button>
+            <el-button
+              v-else
+              type="danger"
+              circle
+              @click="handleCreateChannel(scope.row, PayChannelEnum.WX_LITE.code, PayType.WECHAT)"
+            >
+              <Icon icon="ep:close" />
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column :label="PayChannelEnum.WX_PUB.name" align="center">
+          <template #default="scope">
+            <el-button
+              type="success"
+              circle
+              v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.WX_PUB.code)"
+              @click="handleUpdateChannel(scope.row, PayChannelEnum.WX_PUB.code, PayType.WECHAT)"
+            >
+              <Icon icon="ep:check" />
+            </el-button>
+            <el-button
+              v-else
+              type="danger"
+              circle
+              @click="handleCreateChannel(scope.row, PayChannelEnum.WX_PUB.code, PayType.WECHAT)"
+            >
+              <Icon icon="ep:close" />
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column :label="PayChannelEnum.WX_APP.name" align="center">
+          <template #default="scope">
+            <el-button
+              type="success"
+              circle
+              v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.WX_APP.code)"
+              @click="handleUpdateChannel(scope.row, PayChannelEnum.WX_APP.code, PayType.WECHAT)"
+            >
+              <Icon icon="ep:check" />
+            </el-button>
+            <el-button
+              v-else
+              type="danger"
+              circle
+              @click="handleCreateChannel(scope.row, PayChannelEnum.WX_APP.code, PayType.WECHAT)"
+            >
+              <Icon icon="ep:close" />
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
       />
-      <!-- 按钮:关闭 -->
-      <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
-    </template>
-  </XModal>
+      <el-table-column label="操作" align="center" min-width="110" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['system:tenant:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['system:tenant:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <AppForm ref="formRef" @success="getList" />
 </template>
 <script setup lang="ts" name="PayApp">
-import type { FormExpose } from '@/components/Form'
-import { rules, allSchemas } from './app.data'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
 import * as AppApi from '@/api/pay/app'
-
-const { t } = useI18n() // 国际化
+import AppForm from '@/views/pay/app/AppForm.vue'
+import { PayChannelEnum, PayType } from '@/utils/constants'
 const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
 
-// 列表相关的变量
-const [registerTable, { reload, deleteData, exportList }] = useXTable({
-  allSchemas: allSchemas,
-  getListApi: AppApi.getAppPage,
-  deleteApi: AppApi.deleteApp,
-  exportListApi: AppApi.exportApp
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  remark: undefined,
+  payNotifyUrl: undefined,
+  refundNotifyUrl: undefined,
+  merchantName: undefined,
+  createTime: []
 })
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+const channelParam = reactive({
+  loading: false,
+  edit: false, // 是否修改
+  wechatOpen: false, // 微信是否显示
+  aliPayOpen: false, // 支付宝是否显示
+  appId: null, // 应用 ID
+  payCode: null, // 渠道编码
+  // 商户对象
+  payMerchant: {
+    id: null, // 编号
+    name: null // 名称
+  }
+}) // 微信组件传参参数
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await AppApi.getAppPage(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 formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
 
-// ========== 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 handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await AppApi.deleteApp(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
 
-// 设置标题
-const setDialogTile = (type: string) => {
-  dialogTitle.value = t('action.' + type)
-  actionType.value = type
-  dialogVisible.value = true
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await AppApi.exportApp(queryParams)
+    download.excel(data, '支付应用信息.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
 }
 
-// 新增操作
-const handleCreate = () => {
-  setDialogTile('create')
+/**
+ * 根据渠道编码判断渠道列表中是否存在
+ *
+ * @param channels 渠道列表
+ * @param channelCode 渠道编码
+ */
+const isChannelExists = (channels, channelCode) => {
+  if (!channels) {
+    return false
+  }
+  return channels.indexOf(channelCode) !== -1
 }
 
-// 修改操作
-const handleUpdate = async (rowId: number) => {
-  setDialogTile('update')
-  // 设置数据
-  const res = await AppApi.getApp(rowId)
-  unref(formRef)?.setValues(res)
+// TODO @芋艿:handleUpdateChannel 和 handleCreateChannel 合并,成为 openChannelForm
+/**
+ * 修改支付渠道信息
+ *
+ * @param row 行记录
+ * @param payCode 支付编码
+ * @param type 支付类型
+ */
+const handleUpdateChannel = async (row, payCode, type) => {
+  // TODO @芋艿:表单未实现
+  message.alert('待实现')
+  await settingChannelParam(row, payCode, type)
+  channelParam.edit = true
+  channelParam.loading = true
 }
 
-// 详情操作
-const handleDetail = async (rowId: number) => {
-  setDialogTile('detail')
-  const res = await AppApi.getApp(rowId)
-  detailData.value = res
+/**
+ * 新增支付渠道信息
+ */
+const handleCreateChannel = async (row, payCode, type) => {
+  message.alert('待实现')
+  await settingChannelParam(row, payCode, type)
+  channelParam.edit = false
+  channelParam.loading = false
 }
 
-// 提交按钮
-const submitForm = async () => {
-  const elForm = unref(formRef)?.getElFormRef()
-  if (!elForm) return
-  elForm.validate(async (valid) => {
-    if (valid) {
-      actionLoading.value = true
-      // 提交请求
-      try {
-        const data = unref(formRef)?.formModel as AppApi.AppVO
-        if (actionType.value === 'create') {
-          await AppApi.createApp(data)
-          message.success(t('common.createSuccess'))
-        } else {
-          await AppApi.updateApp(data)
-          message.success(t('common.updateSuccess'))
-        }
-        dialogVisible.value = false
-      } finally {
-        actionLoading.value = false
-        // 刷新列表
-        await reload()
-      }
-    }
-  })
+const settingChannelParam = async (row, payCode, type) => {
+  if (type === PayType.WECHAT) {
+    channelParam.wechatOpen = true
+    channelParam.aliPayOpen = false
+  }
+  if (type === PayType.ALIPAY) {
+    channelParam.aliPayOpen = true
+    channelParam.wechatOpen = false
+  }
+  channelParam.edit = false
+  channelParam.loading = false
+  channelParam.appId = row.id
+  channelParam.payCode = payCode
+  channelParam.payMerchant = row.payMerchant
 }
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+})
 </script>

+ 115 - 0
src/views/pay/order/OrderDetail.vue

@@ -0,0 +1,115 @@
+<template>
+  <Dialog title="详情" v-model="dialogVisible" width="50%">
+    <el-descriptions :column="2">
+      <el-descriptions-item label="商户名称">{{ detailData.merchantName }}</el-descriptions-item>
+      <el-descriptions-item label="应用名称">{{ detailData.appName }}</el-descriptions-item>
+      <el-descriptions-item label="商品名称">{{ detailData.subject }}</el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2">
+      <el-descriptions-item label="商户订单号">
+        <el-tag>{{ detailData.merchantOrderId }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="渠道订单号">
+        <el-tag class="tag-purple">{{ detailData.channelOrderNo }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="支付订单号">
+        <el-tag v-if="detailData.payOrderExtension" class="tag-pink">
+          {{ detailData.payOrderExtension.no }}
+        </el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="金额">
+        <el-tag type="success">¥{{ parseFloat(detailData.amount / 100, 2).toFixed(2) }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="手续费">
+        <el-tag type="warning">
+          ¥{{ parseFloat(detailData.channelFeeAmount / 100, 2).toFixed(2) }}
+        </el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="手续费比例">
+        {{ parseFloat(detailData.channelFeeRate / 100, 2) }}%
+      </el-descriptions-item>
+      <el-descriptions-item label="支付状态">
+        <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="detailData.status" />
+      </el-descriptions-item>
+      <el-descriptions-item label="回调状态">
+        <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="detailData.notifyStatus" />
+      </el-descriptions-item>
+      <el-descriptions-item label="回调地址">{{ detailData.notifyUrl }}</el-descriptions-item>
+      <el-descriptions-item label="创建时间">
+        {{ formatDate(detailData.createTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="支付时间">
+        {{ formatDate(detailData.successTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="失效时间">
+        {{ formatDate(detailData.expireTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="通知时间">
+        {{ formatDate(detailData.notifyTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2">
+      <el-descriptions-item label="支付渠道"
+        >{{ detailData.channelCodeName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="支付IP">{{ detailData.userIp }}</el-descriptions-item>
+      <el-descriptions-item label="退款状态">
+        <dict-tag :type="DICT_TYPE.PAY_ORDER_REFUND_STATUS" :value="detailData.refundStatus" />
+      </el-descriptions-item>
+      <el-descriptions-item label="退款次数">{{ detailData.refundTimes }}</el-descriptions-item>
+      <el-descriptions-item label="退款金额">
+        <el-tag type="warning">
+          {{ parseFloat(detailData.refundAmount / 100, 2) }}
+        </el-tag>
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="1" direction="vertical" border>
+      <el-descriptions-item label="商品描述">
+        {{ detailData.body }}
+      </el-descriptions-item>
+      <el-descriptions-item label="支付通道异步回调内容">
+        <div style="width: 700px; overflow: auto">
+          {{ detailData.payOrderExtension?.channelNotifyData }}
+        </div>
+      </el-descriptions-item>
+    </el-descriptions>
+  </Dialog>
+</template>
+<script setup lang="ts" name="orderForm">
+import { DICT_TYPE } from '@/utils/dict'
+import * as OrderApi from '@/api/pay/order'
+import { formatDate } from '@/utils/formatTime'
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const detailLoading = ref(false) // 表单的加载中
+const detailData = ref({})
+
+/** 打开弹窗 */
+const open = async (id: number) => {
+  dialogVisible.value = true
+  // 设置数据
+  detailLoading.value = true
+  try {
+    detailData.value = await OrderApi.getOrderDetail(id)
+  } finally {
+    detailLoading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>
+<style>
+.tag-purple {
+  color: #722ed1;
+  background: #f9f0ff;
+  border-color: #d3adf7;
+}
+
+.tag-pink {
+  color: #eb2f96;
+  background: #fff0f6;
+  border-color: #ffadd2;
+}
+</style>

+ 319 - 62
src/views/pay/order/index.vue

@@ -1,79 +1,336 @@
 <template>
   <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <template #toolbar_buttons>
-        <!-- 操作:新增 -->
-        <XButton
-          type="primary"
-          preIcon="ep:zoom-in"
-          :title="t('action.add')"
-          v-hasPermi="['pay:order:create']"
-          @click="handleCreate()"
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="所属商户" prop="merchantId">
+        <el-select
+          v-model="queryParams.merchantId"
+          clearable
+          placeholder="请选择所属商户"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="item in merchantList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="应用编号" prop="appId">
+        <el-select
+          clearable
+          v-model="queryParams.appId"
+          placeholder="请选择应用信息"
+          class="!w-240px"
+        >
+          <el-option v-for="item in appList" :key="item.id" :label="item.name" :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="渠道编码" prop="channelCode">
+        <el-select
+          v-model="queryParams.channelCode"
+          placeholder="请输入渠道编码"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="商户订单编号" prop="merchantOrderId">
+        <el-input
+          v-model="queryParams.merchantOrderId"
+          placeholder="请输入商户订单编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
         />
-        <!-- 操作:导出 -->
-        <XButton
-          type="warning"
-          preIcon="ep:download"
-          :title="t('action.export')"
-          v-hasPermi="['pay:order:export']"
-          @click="exportList('订单数据.xls')"
+      </el-form-item>
+      <el-form-item label="渠道订单号" prop="channelOrderNo">
+        <el-input
+          v-model="queryParams.channelOrderNo"
+          placeholder="请输入渠道订单号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
         />
-      </template>
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:详情 -->
-        <XTextButton
-          preIcon="ep:view"
-          :title="t('action.detail')"
-          v-hasPermi="['pay:order:query']"
-          @click="handleDetail(row.id)"
+      </el-form-item>
+      <el-form-item label="支付状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择支付状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PAY_ORDER_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="退款状态" prop="refundStatus">
+        <el-select
+          v-model="queryParams.refundStatus"
+          placeholder="请选择退款状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PAY_ORDER_REFUND_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="回调商户状态" prop="notifyStatus">
+        <el-select
+          v-model="queryParams.notifyStatus"
+          placeholder="请选择订单回调商户状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PAY_ORDER_NOTIFY_STATUS)"
+            :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>
+      </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="['system:tenant:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
-  <XModal v-model="dialogVisible" :title="dialogTitle">
-    <!-- 对话框(详情) -->
-    <Descriptions :schema="allSchemas.detailSchema" :data="detailData" />
-    <!-- 操作按钮 -->
-    <template #footer>
-      <!-- 按钮:关闭 -->
-      <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
-    </template>
-  </XModal>
+
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="订单编号" align="center" prop="id" />
+      <el-table-column label="商户名称" align="center" prop="merchantName" width="120" />
+      <el-table-column label="应用名称" align="center" prop="appName" width="120" />
+      <el-table-column label="渠道名称" align="center" prop="channelCodeName" width="120" />
+      <el-table-column label="渠道订单号" align="center" prop="merchantOrderId" width="120" />
+      <el-table-column label="商品标题" align="center" prop="subject" width="250" />
+      <el-table-column label="商品描述" align="center" prop="body" width="250" />
+      <el-table-column label="异步通知地址" align="center" prop="notifyUrl" width="250" />
+      <el-table-column label="回调状态" align="center" prop="notifyStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="scope.row.notifyStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column label="支付订单" width="280">
+        <template #default="scope">
+          <p class="order-font">
+            <el-tag>商户</el-tag>
+            {{ scope.row.merchantOrderId }}
+          </p>
+          <p class="order-font">
+            <el-tag type="warning">支付</el-tag>
+            {{ scope.row.channelOrderNo }}
+          </p>
+        </template>
+      </el-table-column>
+      <el-table-column label="支付金额" align="center" prop="amount">
+        <template #default="scope">
+          ¥{{ parseFloat(scope.row.amount / 100).toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="手续金额" align="center" prop="channelFeeAmount">
+        <template #default="scope">
+          ¥{{ parseFloat(scope.row.channelFeeAmount / 100).toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="退款金额" align="center" prop="refundAmount">
+        <template #default="scope">
+          ¥{{ parseFloat(scope.row.refundAmount / 100).toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="支付状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="回调状态" align="center" prop="notifyStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="scope.row.notifyStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="支付时间"
+        align="center"
+        prop="successTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center" fixed="right">
+        <template #default="scope">
+          <el-button
+            type="primary"
+            link
+            @click="openDetail(scope.row.id)"
+            v-hasPermi="['pay:order:query']"
+          >
+            详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:预览 -->
+  <OrderDetail ref="detailRef" @success="getList" />
 </template>
 <script setup lang="ts" name="PayOrder">
-import { allSchemas } from './order.data'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as MerchantApi from '@/api/pay/merchant'
 import * as OrderApi from '@/api/pay/order'
+import OrderDetail from './OrderDetail.vue'
+const message = useMessage() // 消息弹窗
+import download from '@/utils/download'
 
-const { t } = useI18n() // 国际化
-// 列表相关的变量
-const [registerTable, { exportList }] = useXTable({
-  allSchemas: allSchemas,
-  getListApi: OrderApi.getOrderPage,
-  exportListApi: OrderApi.exportOrder
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  merchantId: undefined,
+  appId: undefined,
+  channelId: undefined,
+  channelCode: undefined,
+  merchantOrderId: undefined,
+  subject: undefined,
+  body: undefined,
+  notifyUrl: undefined,
+  notifyStatus: undefined,
+  amount: undefined,
+  channelFeeRate: undefined,
+  channelFeeAmount: undefined,
+  status: undefined,
+  userIp: undefined,
+  successExtensionId: undefined,
+  refundStatus: undefined,
+  refundTimes: undefined,
+  refundAmount: undefined,
+  channelUserId: undefined,
+  channelOrderNo: undefined,
+  expireTime: [],
+  successTime: [],
+  notifyTime: [],
+  createTime: []
 })
-// ========== CRUD 相关 ==========
-const actionLoading = ref(false) // 遮罩层
-const actionType = ref('') // 操作按钮的类型
-const dialogVisible = ref(false) // 是否显示弹出层
-const dialogTitle = ref('edit') // 弹出层标题
-const detailData = ref() // 详情 Ref
-// 设置标题
-const setDialogTile = (type: string) => {
-  dialogTitle.value = t('action.' + type)
-  actionType.value = type
-  dialogVisible.value = true
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出等待
+const merchantList = ref([]) // 商户列表
+const appList = ref([]) // 支付应用列表集合
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await OrderApi.getOrderPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
 }
 
-// 新增操作
-const handleCreate = () => {
-  setDialogTile('create')
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await OrderApi.exportOrder(queryParams)
+    download.excel(data, '支付订单.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
 }
 
-// 详情操作
-const handleDetail = async (rowId: number) => {
-  setDialogTile('detail')
-  const res = await OrderApi.getOrder(rowId)
-  detailData.value = res
+/** 预览详情 */
+const detailRef = ref()
+const openDetail = (id: number) => {
+  detailRef.value.open(id)
 }
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  // 加载商户列表
+  merchantList.value = await MerchantApi.getMerchantListByName()
+  // 加载 App 列表
+  // TODO 芋艿:候选少一个查询应用列表的接口
+  // appList.value = await AppApi.getAppListByMerchantId()
+})
 </script>
+<style>
+.order-font {
+  font-size: 12px;
+  padding: 2px 0;
+}
+</style>

+ 0 - 152
src/views/pay/order/order.data.ts

@@ -1,152 +0,0 @@
-import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
-const { t } = useI18n() // 国际化
-
-// 表单校验
-export const rules = reactive({
-  merchantId: [required],
-  appId: [required],
-  merchantOrderId: [required],
-  subject: [required],
-  body: [required],
-  notifyUrl: [required],
-  notifyStatus: [required],
-  amount: [required],
-  status: [required],
-  userIp: [required],
-  expireTime: [required],
-  refundStatus: [required],
-  refundTimes: [required],
-  refundAmount: [required]
-})
-// CrudSchema
-const crudSchemas = reactive<VxeCrudSchema>({
-  primaryKey: 'id',
-  primaryType: 'seq',
-  primaryTitle: '岗位编号',
-  action: true,
-  columns: [
-    {
-      title: '商户编号',
-      field: 'merchantId',
-      isSearch: true
-    },
-    {
-      title: '应用编号',
-      field: 'appId',
-      isSearch: true
-    },
-    {
-      title: '渠道编号',
-      field: 'channelId'
-    },
-    {
-      title: '渠道编码',
-      field: 'channelCode',
-      isSearch: true
-    },
-    {
-      title: '渠道订单号',
-      field: 'merchantOrderId',
-      isSearch: true
-    },
-    {
-      title: '商品标题',
-      field: 'subject'
-    },
-    {
-      title: '商品描述',
-      field: 'body'
-    },
-    {
-      title: '异步通知地址',
-      field: 'notifyUrl'
-    },
-    {
-      title: '回调状态',
-      field: 'notifyStatus',
-      dictType: DICT_TYPE.PAY_ORDER_NOTIFY_STATUS,
-      dictClass: 'number'
-    },
-    {
-      title: '支付金额',
-      field: 'amount',
-      isSearch: true
-    },
-    {
-      title: '渠道手续费',
-      field: 'channelFeeRate',
-      isSearch: true
-    },
-    {
-      title: '渠道手续金额',
-      field: 'channelFeeAmount',
-      isSearch: true
-    },
-    {
-      title: '支付状态',
-      field: 'status',
-      dictType: DICT_TYPE.PAY_ORDER_STATUS,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: '用户 IP',
-      field: 'userIp'
-    },
-    {
-      title: '订单失效时间',
-      field: 'expireTime',
-      formatter: 'formatDate'
-    },
-    {
-      title: '支付时间',
-      field: 'successTime',
-      formatter: 'formatDate'
-    },
-    {
-      title: '支付通知时间',
-      field: 'notifyTime',
-      formatter: 'formatDate'
-    },
-    {
-      title: '拓展编号',
-      field: 'successExtensionId'
-    },
-    {
-      title: '退款状态',
-      field: 'refundStatus',
-      dictType: DICT_TYPE.PAY_ORDER_REFUND_STATUS,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: '退款次数',
-      field: 'refundTimes'
-    },
-    {
-      title: '退款总金额',
-      field: 'refundAmount'
-    },
-    {
-      title: '渠道用户编号',
-      field: 'channelUserId'
-    },
-    {
-      title: '渠道订单号',
-      field: 'channelOrderNo'
-    },
-    {
-      title: t('common.createTime'),
-      field: 'createTime',
-      formatter: 'formatDate',
-      isForm: false,
-      search: {
-        show: true,
-        itemRender: {
-          name: 'XDataTimePicker'
-        }
-      }
-    }
-  ]
-})
-export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

+ 115 - 0
src/views/pay/refund/RefundDetail.vue

@@ -0,0 +1,115 @@
+<template>
+  <Dialog title="详情" v-model="dialogVisible" width="50%">
+    <el-descriptions :column="2">
+      <el-descriptions-item label="商户名称">{{ detailData.merchantName }}</el-descriptions-item>
+      <el-descriptions-item label="应用名称">{{ detailData.appName }}</el-descriptions-item>
+      <el-descriptions-item label="商品名称">{{ detailData.subject }}</el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2">
+      <el-descriptions-item label="商户退款单号">
+        <el-tag>{{ detailData.merchantRefundNo }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="商户订单号">
+        {{ detailData.merchantOrderId }}
+      </el-descriptions-item>
+      <el-descriptions-item label="交易订单号">{{ detailData.tradeNo }}</el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2">
+      <el-descriptions-item label="支付金额">
+        <el-tag type="success">¥{{ parseFloat(detailData.payAmount / 100, 2).toFixed(2) }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="退款金额">
+        <el-tag class="tag-purple">
+          ¥{{ parseFloat(detailData.refundAmount / 100).toFixed(2) }}
+        </el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="退款类型">
+        <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_TYPE" :value="detailData.type" />
+      </el-descriptions-item>
+      <el-descriptions-item label="退款状态">
+        <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_STATUS" :value="detailData.status" />
+      </el-descriptions-item>
+      <el-descriptions-item label="创建时间">
+        {{ formatDate(detailData.createTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="退款成功时间">
+        {{ formatDate(detailData.successTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="退款失效时间">
+        {{ formatDate(detailData.expireTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="更新时间">
+        {{ formatDate(detailData.updateTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2">
+      <el-descriptions-item label="支付渠道">
+        {{ detailData.channelCodeName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="支付 IP">
+        {{ detailData.userIp }}
+      </el-descriptions-item>
+      <el-descriptions-item label="回调地址">{{ detailData.notifyUrl }}</el-descriptions-item>
+      <el-descriptions-item label="回调状态">
+        <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="detailData.notifyStatus" />
+      </el-descriptions-item>
+      <el-descriptions-item label="回调时间">
+        {{ formatDate(detailData.notifyTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-divider />
+    <el-descriptions :column="2">
+      <el-descriptions-item label="渠道订单号">
+        {{ detailData.channelOrderNo }}
+      </el-descriptions-item>
+      <el-descriptions-item label="渠道退款单号">
+        {{ detailData.channelRefundNo }}
+      </el-descriptions-item>
+      <el-descriptions-item label="渠道错误码">
+        {{ detailData.channelErrorCode }}
+      </el-descriptions-item>
+      <el-descriptions-item label="渠道错误码描述">
+        {{ detailData.channelErrorMsg }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <br />
+    <el-descriptions :column="1" direction="vertical" border>
+      <el-descriptions-item label="渠道额外参数">
+        {{ detailData.channelExtras }}
+      </el-descriptions-item>
+      <el-descriptions-item label="退款原因">{{ detailData.reason }}</el-descriptions-item>
+    </el-descriptions>
+  </Dialog>
+</template>
+<script setup lang="ts" name="refundForm">
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as RefundApi from '@/api/pay/refund'
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const detailLoading = ref(false) // 表单的加载中
+const detailData = ref({})
+
+/** 打开弹窗 */
+const open = async (id: number) => {
+  dialogVisible.value = true
+  // 设置数据
+  detailLoading.value = true
+  try {
+    detailData.value = await RefundApi.getRefund(id)
+  } finally {
+    detailLoading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>
+<style>
+.tag-purple {
+  color: #722ed1;
+  background: #f9f0ff;
+  border-color: #d3adf7;
+}
+</style>

+ 326 - 44
src/views/pay/refund/index.vue

@@ -1,59 +1,341 @@
 <template>
+  <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- 列表 -->
-    <XTable @register="registerTable">
-      <template #toolbar_buttons>
-        <!-- 操作:导出 -->
-        <XButton
-          type="warning"
-          preIcon="ep:download"
-          :title="t('action.export')"
-          v-hasPermi="['pay:refund:export']"
-          @click="exportList('退款订单.xls')"
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="120px"
+    >
+      <el-form-item label="所属商户" prop="merchantId">
+        <el-select
+          v-model="queryParams.merchantId"
+          clearable
+          placeholder="请选择所属商户"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="item in merchantList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="应用编号" prop="appId">
+        <el-select
+          v-model="queryParams.appId"
+          clearable
+          placeholder="请选择应用信息"
+          class="!w-240px"
+        >
+          <el-option v-for="item in appList" :key="item.id" :label="item.name" :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="渠道编码" prop="channelCode">
+        <el-select
+          v-model="queryParams.channelCode"
+          placeholder="请输入渠道编码"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="退款类型" prop="type">
+        <el-select
+          v-model="queryParams.type"
+          placeholder="请选择退款类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PAY_REFUND_ORDER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="商户退款订单号" prop="merchantRefundNo">
+        <el-input
+          v-model="queryParams.merchantRefundNo"
+          placeholder="请输入商户退款订单号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
         />
-      </template>
-      <template #actionbtns_default="{ row }">
-        <!-- 操作:详情 -->
-        <XTextButton
-          preIcon="ep:view"
-          :title="t('action.detail')"
-          v-hasPermi="['pay:refund:query']"
-          @click="handleDetail(row.id)"
+      </el-form-item>
+      <el-form-item label="退款状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择退款状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PAY_REFUND_ORDER_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="退款回调状态" prop="notifyStatus">
+        <el-select
+          v-model="queryParams.notifyStatus"
+          placeholder="请选择通知商户退款结果的回调状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PAY_ORDER_NOTIFY_STATUS)"
+            :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>
+      </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="['system:tenant:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
 
-  <XModal v-model="dialogVisible" :title="t('action.detail')">
-    <!-- 对话框(详情) -->
-    <Descriptions :schema="allSchemas.detailSchema" :data="detailData" />
-    <!-- 操作按钮 -->
-    <template #footer>
-      <el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
-    </template>
-  </XModal>
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="商户名称" align="center" prop="merchantName" width="120" />
+      <el-table-column label="应用名称" align="center" prop="appName" width="120" />
+      <el-table-column label="渠道名称" align="center" prop="channelCodeName" width="120" />
+      <el-table-column label="交易订单号" align="center" prop="tradeNo" width="140" />
+      <el-table-column label="商户订单编号" align="center" prop="merchantOrderId" width="140" />
+      <el-table-column label="商户订单号" align="left" width="230">
+        <template #default="scope">
+          <p class="order-font">
+            <el-tag>退款</el-tag>
+            {{ scope.row.merchantRefundNo }}
+          </p>
+          <p class="order-font">
+            <el-tag type="success">交易</el-tag>
+            {{ scope.row.merchantOrderId }}
+          </p>
+        </template>
+      </el-table-column>
+      <el-table-column label="支付订单号" align="center" prop="merchantRefundNo" width="250">
+        <template #default="scope">
+          <p class="order-font">
+            <el-tag>交易</el-tag>
+            {{ scope.row.tradeNo }}
+          </p>
+          <p class="order-font">
+            <el-tag type="warning">渠道</el-tag>
+            {{ scope.row.channelOrderNo }}
+          </p>
+        </template>
+      </el-table-column>
+      <el-table-column label="支付金额(元)" align="center" prop="payAmount" width="100">
+        <template #default="scope">
+          ¥{{ parseFloat(scope.row.payAmount / 100).toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="退款金额(元)" align="center" prop="refundAmount" width="100">
+        <template #default="scope">
+          ¥{{ parseFloat(scope.row.refundAmount / 100).toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="退款类型" align="center" prop="type" width="80">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="退款状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PAY_REFUND_ORDER_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="回调状态" align="center" prop="notifyStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PAY_ORDER_NOTIFY_STATUS" :value="scope.row.notifyStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="退款原因"
+        align="center"
+        prop="reason"
+        width="140"
+        :show-overflow-tooltip="true"
+      />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="退款成功时间"
+        align="center"
+        prop="successTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center" fixed="right">
+        <template #default="scope">
+          <el-button
+            type="primary"
+            link
+            @click="openDetail(scope.row.id)"
+            v-hasPermi="['pay:order:query']"
+          >
+            详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:预览 -->
+  <RefundDetail ref="detailRef" @success="getList" />
 </template>
 <script setup lang="ts" name="PayRefund">
-import { allSchemas } from './refund.data'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as MerchantApi from '@/api/pay/merchant'
 import * as RefundApi from '@/api/pay/refund'
+import RefundDetail from './RefundDetail.vue'
+const message = useMessage() // 消息弹窗
+import download from '@/utils/download'
 
-const { t } = useI18n() // 国际化
-
-// 列表相关的变量
-const [registerTable, { exportList }] = useXTable({
-  allSchemas: allSchemas,
-  getListApi: RefundApi.getRefundPage,
-  exportListApi: RefundApi.exportRefund
+const loading = ref(false) // 列表遮罩层
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  merchantId: undefined,
+  appId: undefined,
+  channelId: undefined,
+  channelCode: undefined,
+  orderId: undefined,
+  tradeNo: undefined,
+  merchantOrderId: undefined,
+  merchantRefundNo: undefined,
+  notifyUrl: undefined,
+  notifyStatus: undefined,
+  status: undefined,
+  type: undefined,
+  payAmount: undefined,
+  refundAmount: undefined,
+  reason: undefined,
+  userIp: undefined,
+  channelOrderNo: undefined,
+  channelRefundNo: undefined,
+  channelErrorCode: undefined,
+  channelErrorMsg: undefined,
+  channelExtras: undefined,
+  expireTime: [],
+  successTime: [],
+  notifyTime: [],
+  createTime: []
 })
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出等待
+const appList = ref([]) // 支付应用列表集合
+const merchantList = ref([]) // 商户列表
 
-// ========== CRUD 相关 ==========
-const dialogVisible = ref(false) // 是否显示弹出层
-const detailData = ref() // 详情 Ref
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await RefundApi.getRefundPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
 
-// 详情操作
-const handleDetail = async (rowId: number) => {
-  // 设置数据
-  detailData.value = RefundApi.getRefund(rowId)
-  dialogVisible.value = true
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
 }
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await RefundApi.exportRefund(queryParams)
+    download.excel(data, '支付订单.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 预览详情 */
+const detailRef = ref()
+const openDetail = (id: number) => {
+  detailRef.value.open(id)
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  // 加载商户列表
+  merchantList.value = await MerchantApi.getMerchantListByName()
+  // TODO 芋艿:候选少一个查询应用列表的接口
+  // appList.value = await AppApi.getAppListByMerchantId()
+})
 </script>
+
+<style>
+.order-font {
+  font-size: 12px;
+  padding: 2px 0;
+}
+</style>

+ 0 - 173
src/views/pay/refund/refund.data.ts

@@ -1,173 +0,0 @@
-import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
-const { t } = useI18n() // 国际化
-
-// CrudSchema
-const crudSchemas = reactive<VxeCrudSchema>({
-  primaryKey: 'id',
-  primaryType: 'seq',
-  primaryTitle: '序号',
-  action: true,
-  columns: [
-    {
-      title: '商户编号',
-      field: 'merchantId',
-      isSearch: true
-    },
-    {
-      title: '应用编号',
-      field: 'appId',
-      isSearch: true
-    },
-    {
-      title: '渠道编号',
-      field: 'channelId',
-      isSearch: true
-    },
-    {
-      title: '渠道编码',
-      field: 'channelCode',
-      dictType: DICT_TYPE.PAY_CHANNEL_CODE_TYPE,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: '支付订单编号',
-      field: 'orderId',
-      isSearch: true
-    },
-    {
-      title: '交易订单号',
-      field: 'tradeNo',
-      isSearch: true
-    },
-    {
-      title: '商户订单号',
-      field: 'merchantOrderId',
-      isSearch: true
-    },
-    {
-      title: '商户退款单号',
-      field: 'merchantRefundNo',
-      isSearch: true
-    },
-    {
-      title: '回调地址',
-      field: 'notifyUrl',
-      isSearch: true
-    },
-    {
-      title: '回调状态',
-      field: 'notifyStatus',
-      dictType: DICT_TYPE.PAY_ORDER_NOTIFY_STATUS,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: '退款类型',
-      field: 'type',
-      dictType: DICT_TYPE.PAY_REFUND_ORDER_TYPE,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: t('common.status'),
-      field: 'status',
-      dictType: DICT_TYPE.PAY_REFUND_ORDER_STATUS,
-      dictClass: 'number',
-      isSearch: true
-    },
-    {
-      title: '支付金额',
-      field: 'payAmount',
-      formatter: 'formatAmount',
-      isSearch: true
-    },
-    {
-      title: '退款金额',
-      field: 'refundAmount',
-      formatter: 'formatAmount',
-      isSearch: true
-    },
-    {
-      title: '退款原因',
-      field: 'reason',
-      isSearch: true
-    },
-    {
-      title: '用户IP',
-      field: 'userIp',
-      isSearch: true
-    },
-    {
-      title: '渠道订单号',
-      field: 'channelOrderNo',
-      isSearch: true
-    },
-    {
-      title: '渠道退款单号',
-      field: 'channelRefundNo',
-      isSearch: true
-    },
-    {
-      title: '渠道调用报错时',
-      field: 'channelErrorCode'
-    },
-    {
-      title: '渠道调用报错时',
-      field: 'channelErrorMsg'
-    },
-    {
-      title: '支付渠道的额外参数',
-      field: 'channelExtras'
-    },
-    {
-      title: '退款失效时间',
-      field: 'expireTime',
-      formatter: 'formatDate',
-      isForm: false,
-      search: {
-        show: true,
-        itemRender: {
-          name: 'XDataTimePicker'
-        }
-      }
-    },
-    {
-      title: '退款成功时间',
-      field: 'successTime',
-      formatter: 'formatDate',
-      isForm: false,
-      search: {
-        show: true,
-        itemRender: {
-          name: 'XDataTimePicker'
-        }
-      }
-    },
-    {
-      title: '退款通知时间',
-      field: 'notifyTime',
-      formatter: 'formatDate',
-      isForm: false,
-      search: {
-        show: true,
-        itemRender: {
-          name: 'XDataTimePicker'
-        }
-      }
-    },
-    {
-      title: t('common.createTime'),
-      field: 'createTime',
-      formatter: 'formatDate',
-      isForm: false,
-      search: {
-        show: true,
-        itemRender: {
-          name: 'XDataTimePicker'
-        }
-      }
-    }
-  ]
-})
-export const { allSchemas } = useVxeCrudSchemas(crudSchemas)

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

@@ -19,7 +19,7 @@
       <el-form-item label="部门状态" prop="status">
         <el-select
           v-model="queryParams.status"
-          placeholder="请选择不么你状态"
+          placeholder="请选择部门状态"
           clearable
           class="!w-240px"
         >

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

@@ -115,11 +115,12 @@ const colorTypeOptions = readonly([
 ])
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number) => {
+const open = async (type: string, id?: number, dictType?: string) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
+  formData.value.dictType = dictType
   // 修改时,设置数据
   if (id) {
     formLoading.value = true

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

@@ -167,7 +167,7 @@ const resetQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+  formRef.value.open(type, id, queryParams.dictType)
 }
 
 /** 删除按钮操作 */

+ 1 - 1
src/views/system/mail/account/account.data.ts

@@ -19,7 +19,7 @@ export const rules = reactive({
   sslEnable: [required]
 })
 
-// CrudSchema:https://kailong110120130.gitee.io/vue-element-plus-admin-doc/hooks/useCrudSchemas.html
+// CrudSchema:https://doc.iocoder.cn/vue3/crud-schema/
 const crudSchemas = reactive<CrudSchema[]>([
   {
     label: '邮箱',

+ 1 - 1
src/views/system/mail/account/index.vue

@@ -72,7 +72,7 @@ import MailAccountDetail from './MailAccountDetail.vue'
 
 // tableObject:表格的属性对象,可获得分页大小、条数等属性
 // tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
-// 详细可见:https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/table.html#usetable
+// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
 const { tableObject, tableMethods } = useTable({
   getListApi: MailAccountApi.getMailAccountPage, // 分页接口
   delListApi: MailAccountApi.deleteMailAccount // 删除接口

+ 1 - 1
src/views/system/mail/log/index.vue

@@ -41,7 +41,7 @@ import MailLogDetail from './MailLogDetail.vue'
 
 // tableObject:表格的属性对象,可获得分页大小、条数等属性
 // tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
-// 详细可见:https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/table.html#usetable
+// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
 const { tableObject, tableMethods } = useTable({
   getListApi: MailLogApi.getMailLogPage // 分页接口
 })

+ 1 - 1
src/views/system/mail/log/log.data.ts

@@ -5,7 +5,7 @@ import * as MailAccountApi from '@/api/system/mail/account'
 // 邮箱账号的列表
 const accountList = await MailAccountApi.getSimpleMailAccountList()
 
-// CrudSchema:https://kailong110120130.gitee.io/vue-element-plus-admin-doc/hooks/useCrudSchemas.html
+// CrudSchema:https://doc.iocoder.cn/vue3/crud-schema/
 const crudSchemas = reactive<CrudSchema[]>([
   {
     label: '编号',

+ 1 - 1
src/views/system/mail/template/index.vue

@@ -73,7 +73,7 @@ import MailTemplateSendForm from './MailTemplateSendForm.vue'
 
 // tableObject:表格的属性对象,可获得分页大小、条数等属性
 // tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
-// 详细可见:https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/table.html#usetable
+// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
 const { tableObject, tableMethods } = useTable({
   getListApi: MailTemplateApi.getMailTemplatePage, // 分页接口
   delListApi: MailTemplateApi.deleteMailTemplate // 删除接口

+ 1 - 1
src/views/system/mail/template/template.data.ts

@@ -17,7 +17,7 @@ export const rules = reactive({
   status: [required]
 })
 
-// CrudSchema:https://kailong110120130.gitee.io/vue-element-plus-admin-doc/hooks/useCrudSchemas.html
+// CrudSchema:https://doc.iocoder.cn/vue3/crud-schema/
 const crudSchemas = reactive<CrudSchema[]>([
   {
     label: '模板编码',

+ 4 - 4
src/views/system/notify/template/index.vue

@@ -46,7 +46,7 @@
   </ContentWrap>
 
   <!-- 添加/修改的弹窗 -->
-  <XModal id="templateModel" :loading="modelLoading" v-model="dialogVisible" :title="dialogTitle">
+  <Dialog id="templateModel" :loading="modelLoading" v-model="dialogVisible" :title="dialogTitle">
     <!-- 表单:添加/修改 -->
     <Form
       ref="formRef"
@@ -72,10 +72,10 @@
       <!-- 按钮:关闭 -->
       <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
     </template>
-  </XModal>
+  </Dialog>
 
   <!-- 测试站内信的弹窗 -->
-  <XModal id="sendTest" v-model="sendVisible" title="测试">
+  <Dialog id="sendTest" v-model="sendVisible" title="测试">
     <el-form :model="sendForm" :rules="sendRules" label-width="200px" label-position="top">
       <el-form-item label="模板内容" prop="content">
         <el-input type="textarea" v-model="sendForm.content" readonly />
@@ -112,7 +112,7 @@
       />
       <XButton :title="t('dialog.close')" @click="sendVisible = false" />
     </template>
-  </XModal>
+  </Dialog>
 </template>
 <script setup lang="ts" name="SystemNotifyTemplate">
 import { FormExpose } from '@/components/Form'

+ 4 - 1
src/views/system/post/PostForm.vue

@@ -13,6 +13,9 @@
       <el-form-item label="岗位编码" prop="code">
         <el-input v-model="formData.code" placeholder="请输入岗位编码" />
       </el-form-item>
+      <el-form-item label="岗位顺序" prop="sort">
+        <el-input v-model="formData.sort" placeholder="请输入岗位顺序" />
+      </el-form-item>
       <el-form-item label="状态" prop="status">
         <el-select v-model="formData.status" placeholder="请选择状态" clearable>
           <el-option
@@ -49,7 +52,7 @@ const formData = ref({
   id: undefined,
   name: '',
   code: '',
-  sort: undefined,
+  sort: 0,
   status: CommonStatusEnum.ENABLE,
   remark: ''
 })