Browse Source

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

# Conflicts:
#	src/views/Login/components/LoginForm.vue
#	src/views/Login/components/MobileForm.vue
#	src/views/mp/components/wx-editor/WxEditor.vue
puhui999 1 year ago
parent
commit
ec58321dd8
37 changed files with 580 additions and 903 deletions
  1. 11 1
      .vscode/settings.json
  2. 4 5
      package.json
  3. 2 1
      src/components/Editor/src/Editor.vue
  4. 6 3
      src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
  5. 2 0
      src/hooks/web/useCrudSchemas.ts
  6. 5 4
      src/layout/components/AppView.vue
  7. 3 1
      src/layout/components/useRenderLayout.tsx
  8. 0 7
      src/main.ts
  9. 0 1
      src/styles/index.scss
  10. 0 2
      src/styles/variables.scss
  11. 0 30
      src/styles/vxe.css
  12. 1 0
      src/types/.gitignore
  13. 0 128
      src/types/auto-components.d.ts
  14. 0 76
      src/types/auto-imports.d.ts
  15. 0 8
      src/utils/env.ts
  16. 2 1
      src/views/Login/components/LoginForm.vue
  17. 2 1
      src/views/Login/components/MobileForm.vue
  18. 2 1
      src/views/bpm/definition/index.vue
  19. 4 2
      src/views/bpm/model/index.vue
  20. 6 19
      src/views/mp/autoReply/index.vue
  21. 0 36
      src/views/mp/components/WxMpSelect.vue
  22. 21 28
      src/views/mp/components/wx-account-select/main.vue
  23. 0 104
      src/views/mp/components/wx-editor/WxEditor.vue
  24. 0 45
      src/views/mp/components/wx-editor/quill-options.js
  25. 1 1
      src/views/mp/components/wx-reply/main.vue
  26. 75 0
      src/views/mp/draft/editor-config.ts
  27. 237 230
      src/views/mp/draft/index.vue
  28. 2 4
      src/views/mp/freePublish/index.vue
  29. 2 2
      src/views/mp/material/index.vue
  30. 2 2
      src/views/mp/menu/index.vue
  31. 145 0
      src/views/mp/message/MessageTable.vue
  32. 34 153
      src/views/mp/message/index.vue
  33. 2 2
      src/views/mp/tag/index.vue
  34. 2 2
      src/views/mp/user/index.vue
  35. 2 1
      src/views/system/dict/DictTypeForm.vue
  36. 2 1
      src/views/system/dict/data/DictDataForm.vue
  37. 3 1
      src/views/system/tenant/TenantForm.vue

+ 11 - 1
.vscode/settings.json

@@ -40,5 +40,15 @@
   "i18n-ally.displayLanguage": "zh-CN",
   "i18n-ally.enabledFrameworks": ["vue", "react"],
   "god.tsconfig": "./tsconfig.json",
-  "vue-i18n.i18nPaths": "src/locales"
+  "vue-i18n.i18nPaths": "src/locales",
+  "explorer.fileNesting.enabled": true,
+  "explorer.fileNesting.expand": false,
+  "explorer.fileNesting.patterns": {
+    "*.ts": "$(capture).test.ts, $(capture).test.tsx",
+    "*.tsx": "$(capture).test.ts, $(capture).test.tsx",
+    "*.env": "$(capture).env.*",
+    "CHANGELOG.md": "CHANGELOG*",
+    "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,README*,.npmrc,.browserslistrc,vite.config.*,windi.*,tailwind.*,tsconfig.*,postcss*",
+    ".eslintrc.js": ".eslintignore,.eslintrc-*,.prettierignore,.stylelintignore,.commitlintrc.js,.prettierrc.js,.stylelint*,stylelint*,prettier.*,.editorconfig"
+  }
 }

+ 4 - 5
package.json

@@ -32,13 +32,12 @@
     "@form-create/element-ui": "^3.1.17",
     "@iconify/iconify": "^3.1.0",
     "@videojs-player/vue": "^1.0.0",
-    "@vueup/vue-quill": "^1.1.1",
     "@vueuse/core": "^9.13.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.10",
     "@zxcvbn-ts/core": "^2.2.1",
     "animate.css": "^4.1.1",
-    "axios": "^1.3.4",
+    "axios": "^1.3.5",
     "benz-amr-recorder": "^1.1.5",
     "bpmn-js-token-simulation": "^0.10.0",
     "camunda-bpmn-moddle": "^7.0.1",
@@ -48,16 +47,16 @@
     "diagram-js": "^11.6.0",
     "echarts": "^5.4.1",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.3.1",
+    "element-plus": "2.3.3",
     "fast-xml-parser": "^4.1.3",
     "highlight.js": "^11.7.0",
-    "intro.js": "^6.0.0",
+    "intro.js": "^7.0.1",
     "jsencrypt": "^3.3.2",
     "lodash-es": "^4.17.21",
     "min-dash": "^4.0.0",
     "mitt": "^3.0.0",
     "nprogress": "^0.2.0",
-    "pinia": "^2.0.33",
+    "pinia": "^2.0.34",
     "qrcode": "^1.5.1",
     "qs": "^6.11.1",
     "steady-xml": "^0.1.0",

+ 2 - 1
src/components/Editor/src/Editor.vue

@@ -20,7 +20,7 @@ const props = defineProps({
   editorId: propTypes.string.def('wangeEditor-1'),
   height: propTypes.oneOfType([Number, String]).def('500px'),
   editorConfig: {
-    type: Object as PropType<IEditorConfig>,
+    type: Object as PropType<Partial<IEditorConfig>>,
     default: () => undefined
   },
   readonly: propTypes.bool.def(false),
@@ -147,6 +147,7 @@ const editorConfig = computed((): IEditorConfig => {
     props.editorConfig || {}
   )
 })
+
 const editorStyle = computed(() => {
   return {
     height: isNumber(props.height) ? `${props.height}px` : props.height

+ 6 - 3
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue

@@ -111,7 +111,8 @@
               >
               <el-divider direction="vertical" />
               <el-button
-                type="text"
+                type="primary"
+                link
                 style="color: #ff4d4f"
                 @click="removeFieldOptionItem(scope, scope.$index, 'enum')"
                 >移除</el-button
@@ -143,7 +144,8 @@
             >
             <el-divider direction="vertical" />
             <el-button
-              type="text"
+              type="primary"
+              link
               style="color: #ff4d4f"
               @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"
               >移除</el-button
@@ -174,7 +176,8 @@
             >
             <el-divider direction="vertical" />
             <el-button
-              type="text"
+              type="primary"
+              link
               style="color: #ff4d4f"
               @click="removeFieldOptionItem(scope, scope.$index, 'property')"
               >移除</el-button

+ 2 - 0
src/hooks/web/useCrudSchemas.ts

@@ -29,6 +29,8 @@ type CrudSearchParams = {
   show?: boolean
   // 接口
   api?: () => Promise<any>
+  // 搜索字段
+  field?: string
 } & Omit<FormSchema, 'field'>
 
 type CrudTableParams = {

+ 5 - 4
src/layout/components/AppView.vue

@@ -21,16 +21,17 @@ const getCaches = computed((): string[] => {
 <template>
   <section
     :class="[
-      'p-[var(--app-content-padding)] w-[100%] bg-[var(--app-contnet-bg-color)] dark:bg-[var(--el-bg-color)]',
+      'p-[var(--app-content-padding)] w-[100%] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
       {
         '!min-h-[calc(100%-var(--app-footer-height))]':
-          fixedHeader && (layout === 'classic' || layout === 'topLeft') && footer,
+          ((fixedHeader && (layout === 'classic' || layout === 'topLeft')) || layout === 'top') &&
+          footer,
 
         '!min-h-[calc(100%-var(--tags-view-height)-var(--top-tool-height)-var(--app-footer-height))]':
-          ((!fixedHeader && layout === 'classic') || layout === 'top') && footer,
+          !fixedHeader && layout === 'classic' && footer,
 
         '!min-h-[calc(100%-var(--tags-view-height)-var(--app-footer-height))]':
-          !fixedHeader && layout === 'topLeft' && footer,
+          !fixedHeader && (layout === 'topLeft' || layout === 'top') && footer,
 
         '!min-h-[calc(100%-var(--top-tool-height))]': fixedHeader && layout === 'cutMenu' && footer,
 

+ 3 - 1
src/layout/components/useRenderLayout.tsx

@@ -177,7 +177,9 @@ export const useRenderLayout = () => {
             class={[
               `${prefixCls}-content-scrollbar`,
               {
-                'mt-[var(--tags-view-height)]': fixedHeader.value
+                'mt-[var(--tags-view-height)] !pb-[calc(var(--tags-view-height)+var(--app-footer-height))]':
+                  fixedHeader.value,
+                'pb-[var(--app-footer-height)]': !fixedHeader.value
               }
             ]}
           >

+ 0 - 7
src/main.ts

@@ -37,8 +37,6 @@ import App from './App.vue'
 
 import './permission'
 
-import { isDevMode } from '@/utils/env'
-
 import { MyPD } from '@/components/bpmnProcessDesigner/package/index.js'
 import '@/components/bpmnProcessDesigner/package/theme/index.scss'
 import 'bpmn-js/dist/assets/diagram-js.css'
@@ -53,11 +51,6 @@ import '@/plugins/tongji' // 百度统计
 
 import Logger from '@/utils/Logger'
 
-// 本地开发模式 全局引入 element-plus 样式,加快第一次进入速度
-if (isDevMode()) {
-  import('element-plus/dist/index.css')
-}
-
 // 创建实例
 const setupAll = async () => {
   const app = createApp(App)

+ 0 - 1
src/styles/index.scss

@@ -1,5 +1,4 @@
 @import './var.css';
-@import './vxe.css';
 @import 'element-plus/theme-chalk/dark/css-vars.css';
 
 .reset-margin [class*='el-icon'] + span {

+ 0 - 2
src/styles/variables.scss

@@ -2,5 +2,3 @@
 $namespace: v;
 // el命名空间
 $elNamespace: el;
-// vxe命名空间
-$vxeNamespace: vxe;

+ 0 - 30
src/styles/vxe.css

@@ -1,30 +0,0 @@
-/*滚动条整体部分*/
-.xtable-scrollbar ::-webkit-scrollbar {
-  width: 10px;
-  height: 10px;
-}
-/*滚动条的轨道*/
-.xtable-scrollbar ::-webkit-scrollbar-track {
-  background-color: #ffffff;
-}
-/*滚动条里面的小方块,能向上向下移动*/
-.xtable-scrollbar ::-webkit-scrollbar-thumb {
-  background-color: #bfbfbf;
-  border-radius: 5px;
-  border: 1px solid #f1f1f1;
-  box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-}
-.xtable-scrollbar ::-webkit-scrollbar-thumb:hover {
-  background-color: #a8a8a8;
-}
-.xtable-scrollbar ::-webkit-scrollbar-thumb:active {
-  background-color: #787878;
-}
-/*边角,即两个滚动条的交汇处*/
-.xtable-scrollbar ::-webkit-scrollbar-corner {
-  background-color: #ffffff;
-}
-/*移除 vxe-grid 右侧外框*/
-.vxe-button.size--medium.type--button.is--circle {
-  border: 0;
-}

+ 1 - 0
src/types/.gitignore

@@ -0,0 +1 @@
+auto-*.d.ts

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

@@ -1,128 +0,0 @@
-/* eslint-disable */
-/* prettier-ignore */
-// @ts-nocheck
-// Generated by unplugin-vue-components
-// Read more: https://github.com/vuejs/core/pull/3399
-import '@vue/runtime-core'
-
-export {}
-
-declare module '@vue/runtime-core' {
-  export interface GlobalComponents {
-    Backtop: typeof import('./../components/Backtop/src/Backtop.vue')['default']
-    ConfigGlobal: typeof import('./../components/ConfigGlobal/src/ConfigGlobal.vue')['default']
-    ContentDetailWrap: typeof import('./../components/ContentDetailWrap/src/ContentDetailWrap.vue')['default']
-    ContentWrap: typeof import('./../components/ContentWrap/src/ContentWrap.vue')['default']
-    CopperModal: typeof import('./../components/Cropper/src/CopperModal.vue')['default']
-    CountTo: typeof import('./../components/CountTo/src/CountTo.vue')['default']
-    Crontab: typeof import('./../components/Crontab/src/Crontab.vue')['default']
-    Cropper: typeof import('./../components/Cropper/src/Cropper.vue')['default']
-    CropperAvatar: typeof import('./../components/Cropper/src/CropperAvatar.vue')['default']
-    Descriptions: typeof import('./../components/Descriptions/src/Descriptions.vue')['default']
-    Dialog: typeof import('./../components/Dialog/src/Dialog.vue')['default']
-    DictTag: typeof import('./../components/DictTag/src/DictTag.vue')['default']
-    DocAlert: typeof import('./../components/DocAlert/index.vue')['default']
-    Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
-    Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
-    ElAlert: typeof import('element-plus/es')['ElAlert']
-    ElAutoResizer: typeof import('element-plus/es')['ElAutoResizer']
-    ElAvatar: typeof import('element-plus/es')['ElAvatar']
-    ElBadge: typeof import('element-plus/es')['ElBadge']
-    ElButton: typeof import('element-plus/es')['ElButton']
-    ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
-    ElCard: typeof import('element-plus/es')['ElCard']
-    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
-    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
-    ElCol: typeof import('element-plus/es')['ElCol']
-    ElCollapse: typeof import('element-plus/es')['ElCollapse']
-    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
-    ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
-    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
-    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
-    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
-    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
-    ElDialog: typeof import('element-plus/es')['ElDialog']
-    ElDivider: typeof import('element-plus/es')['ElDivider']
-    ElDrawer: typeof import('element-plus/es')['ElDrawer']
-    ElDropdown: typeof import('element-plus/es')['ElDropdown']
-    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
-    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
-    ElementBaseInfo: typeof import('./../components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue')['default']
-    ElementForm: typeof import('./../components/bpmnProcessDesigner/package/penal/form/ElementForm.vue')['default']
-    ElementListeners: typeof import('./../components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue')['default']
-    ElementMultiInstance: typeof import('./../components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue')['default']
-    ElementOtherConfig: typeof import('./../components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue')['default']
-    ElementProperties: typeof import('./../components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue')['default']
-    ElementTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/ElementTask.vue')['default']
-    ElForm: typeof import('element-plus/es')['ElForm']
-    ElFormItem: typeof import('element-plus/es')['ElFormItem']
-    ElIcon: typeof import('element-plus/es')['ElIcon']
-    ElImage: typeof import('element-plus/es')['ElImage']
-    ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
-    ElInput: typeof import('element-plus/es')['ElInput']
-    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
-    ElLink: typeof import('element-plus/es')['ElLink']
-    ElOption: typeof import('element-plus/es')['ElOption']
-    ElPagination: typeof import('element-plus/es')['ElPagination']
-    ElPopover: typeof import('element-plus/es')['ElPopover']
-    ElRadio: typeof import('element-plus/es')['ElRadio']
-    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
-    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
-    ElRow: typeof import('element-plus/es')['ElRow']
-    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
-    ElSelect: typeof import('element-plus/es')['ElSelect']
-    ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
-    ElSpace: typeof import('element-plus/es')['ElSpace']
-    ElSwitch: typeof import('element-plus/es')['ElSwitch']
-    ElTable: typeof import('element-plus/es')['ElTable']
-    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
-    ElTableV2: typeof import('element-plus/es')['ElTableV2']
-    ElTabPane: typeof import('element-plus/es')['ElTabPane']
-    ElTabs: typeof import('element-plus/es')['ElTabs']
-    ElTag: typeof import('element-plus/es')['ElTag']
-    ElTimeline: typeof import('element-plus/es')['ElTimeline']
-    ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
-    ElTooltip: typeof import('element-plus/es')['ElTooltip']
-    ElTree: typeof import('element-plus/es')['ElTree']
-    ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
-    ElUpload: typeof import('element-plus/es')['ElUpload']
-    Error: typeof import('./../components/Error/src/Error.vue')['default']
-    FlowCondition: typeof import('./../components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue')['default']
-    Form: typeof import('./../components/Form/src/Form.vue')['default']
-    Highlight: typeof import('./../components/Highlight/src/Highlight.vue')['default']
-    Icon: typeof import('./../components/Icon/src/Icon.vue')['default']
-    IconSelect: typeof import('./../components/Icon/src/IconSelect.vue')['default']
-    IFrame: typeof import('./../components/IFrame/src/IFrame.vue')['default']
-    ImageViewer: typeof import('./../components/ImageViewer/src/ImageViewer.vue')['default']
-    Infotip: typeof import('./../components/Infotip/src/Infotip.vue')['default']
-    InputPassword: typeof import('./../components/InputPassword/src/InputPassword.vue')['default']
-    Pagination: typeof import('./../components/Pagination/index.vue')['default']
-    ProcessDesigner: typeof import('./../components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue')['default']
-    ProcessPalette: typeof import('./../components/bpmnProcessDesigner/package/palette/ProcessPalette.vue')['default']
-    ProcessViewer: typeof import('./../components/bpmnProcessDesigner/package/designer/ProcessViewer.vue')['default']
-    PropertiesPanel: typeof import('./../components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue')['default']
-    Qrcode: typeof import('./../components/Qrcode/src/Qrcode.vue')['default']
-    ReceiveTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue')['default']
-    RouterLink: typeof import('vue-router')['RouterLink']
-    RouterView: typeof import('vue-router')['RouterView']
-    ScriptTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/ScriptTask.vue')['default']
-    Search: typeof import('./../components/Search/src/Search.vue')['default']
-    SignalAndMessage: typeof import('./../components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue')['default']
-    Sticky: typeof import('./../components/Sticky/src/Sticky.vue')['default']
-    Table: typeof import('./../components/Table/src/Table.vue')['default']
-    Tooltip: typeof import('./../components/Tooltip/src/Tooltip.vue')['default']
-    UploadFile: typeof import('./../components/UploadFile/src/UploadFile.vue')['default']
-    UploadImg: typeof import('./../components/UploadFile/src/UploadImg.vue')['default']
-    UploadImgs: typeof import('./../components/UploadFile/src/UploadImgs.vue')['default']
-    UserTask: typeof import('./../components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue')['default']
-    UserTaskListeners: typeof import('./../components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue')['default']
-    Verify: typeof import('./../components/Verifition/src/Verify.vue')['default']
-    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']
-    XTextButton: typeof import('./../components/XButton/src/XTextButton.vue')['default']
-  }
-  export interface ComponentCustomProperties {
-    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
-  }
-}

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

@@ -1,76 +0,0 @@
-/* eslint-disable */
-/* prettier-ignore */
-// @ts-nocheck
-// Generated by unplugin-auto-import
-export {}
-declare global {
-  const DICT_TYPE: typeof import('@/utils/dict')['DICT_TYPE']
-  const EffectScope: typeof import('vue')['EffectScope']
-  const computed: typeof import('vue')['computed']
-  const createApp: typeof import('vue')['createApp']
-  const customRef: typeof import('vue')['customRef']
-  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
-  const defineComponent: typeof import('vue')['defineComponent']
-  const effectScope: typeof import('vue')['effectScope']
-  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
-  const getCurrentScope: typeof import('vue')['getCurrentScope']
-  const h: typeof import('vue')['h']
-  const inject: typeof import('vue')['inject']
-  const isProxy: typeof import('vue')['isProxy']
-  const isReactive: typeof import('vue')['isReactive']
-  const isReadonly: typeof import('vue')['isReadonly']
-  const isRef: typeof import('vue')['isRef']
-  const markRaw: typeof import('vue')['markRaw']
-  const nextTick: typeof import('vue')['nextTick']
-  const onActivated: typeof import('vue')['onActivated']
-  const onBeforeMount: typeof import('vue')['onBeforeMount']
-  const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
-  const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
-  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
-  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
-  const onDeactivated: typeof import('vue')['onDeactivated']
-  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
-  const onMounted: typeof import('vue')['onMounted']
-  const onRenderTracked: typeof import('vue')['onRenderTracked']
-  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
-  const onScopeDispose: typeof import('vue')['onScopeDispose']
-  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
-  const onUnmounted: typeof import('vue')['onUnmounted']
-  const onUpdated: typeof import('vue')['onUpdated']
-  const provide: typeof import('vue')['provide']
-  const reactive: typeof import('vue')['reactive']
-  const readonly: typeof import('vue')['readonly']
-  const ref: typeof import('vue')['ref']
-  const required: typeof import('@/utils/formRules')['required']
-  const resolveComponent: typeof import('vue')['resolveComponent']
-  const shallowReactive: typeof import('vue')['shallowReactive']
-  const shallowReadonly: typeof import('vue')['shallowReadonly']
-  const shallowRef: typeof import('vue')['shallowRef']
-  const toRaw: typeof import('vue')['toRaw']
-  const toRef: typeof import('vue')['toRef']
-  const toRefs: typeof import('vue')['toRefs']
-  const triggerRef: typeof import('vue')['triggerRef']
-  const unref: typeof import('vue')['unref']
-  const useAttrs: typeof import('vue')['useAttrs']
-  const useCrudSchemas: typeof import('@/hooks/web/useCrudSchemas')['useCrudSchemas']
-  const useCssModule: typeof import('vue')['useCssModule']
-  const useCssVars: typeof import('vue')['useCssVars']
-  const useI18n: typeof import('@/hooks/web/useI18n')['useI18n']
-  const useLink: typeof import('vue-router')['useLink']
-  const useMessage: typeof import('@/hooks/web/useMessage')['useMessage']
-  const useRoute: typeof import('vue-router')['useRoute']
-  const useRouter: typeof import('vue-router')['useRouter']
-  const useSlots: typeof import('vue')['useSlots']
-  const useTable: typeof import('@/hooks/web/useTable')['useTable']
-  const useVxeCrudSchemas: typeof import('@/hooks/web/useVxeCrudSchemas')['useVxeCrudSchemas']
-  const useXTable: typeof import('@/hooks/web/useXTable')['useXTable']
-  const watch: typeof import('vue')['watch']
-  const watchEffect: typeof import('vue')['watchEffect']
-  const watchPostEffect: typeof import('vue')['watchPostEffect']
-  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
-}
-// for type re-export
-declare global {
-  // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
-}

+ 0 - 8
src/utils/env.ts

@@ -1,8 +0,0 @@
-export const isDevMode = () => {
-  const dev = import.meta.env.VITE_DEV
-  if (dev && dev === 'true') {
-    return true
-  } else {
-    return false
-  }
-}

+ 2 - 1
src/views/Login/components/LoginForm.vue

@@ -21,7 +21,8 @@
             v-model="loginData.loginForm.tenantName"
             :placeholder="t('login.tenantNamePlaceholder')"
             :prefix-icon="iconHouse"
-            type="text"
+            type="primary"
+            link
           />
         </el-form-item>
       </el-col>

+ 2 - 1
src/views/Login/components/MobileForm.vue

@@ -22,7 +22,8 @@
             v-model="loginData.loginForm.tenantName"
             :placeholder="t('login.tenantNamePlaceholder')"
             :prefix-icon="iconHouse"
-            type="text"
+            type="primary"
+            link
           />
         </el-form-item>
       </el-col>

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

@@ -18,7 +18,8 @@
         <template #default="scope">
           <el-button
             v-if="scope.row.formType === 10"
-            type="text"
+            type="primary"
+            link
             @click="handleFormDetail(scope.row)"
           >
             <span>{{ scope.row.formName }}</span>

+ 4 - 2
src/views/bpm/model/index.vue

@@ -79,14 +79,16 @@
         <template #default="scope">
           <el-button
             v-if="scope.row.formType === 10"
-            type="text"
+            type="primary"
+            link
             @click="handleFormDetail(scope.row)"
           >
             <span>{{ scope.row.formName }}</span>
           </el-button>
           <el-button
             v-else-if="scope.row.formType === 20"
-            type="text"
+            type="primary"
+            link
             @click="handleFormDetail(scope.row)"
           >
             <span>{{ scope.row.formCustomCreatePath }}</span>

+ 6 - 19
src/views/mp/autoReply/index.vue

@@ -5,7 +5,7 @@
   <ContentWrap>
     <el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px">
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -140,7 +140,7 @@
     </el-table>
 
     <!-- 添加或修改自动回复的对话框 -->
-    <el-dialog :title="title" v-model="showReplyFormDialog" width="800px" append-to-body>
+    <el-dialog :title="title" v-model="showReplyFormDialog" width="800px" destroy-on-close>
       <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="请选择">
@@ -167,7 +167,7 @@
           <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
         </el-form-item>
         <el-form-item label="回复消息">
-          <WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" />
+          <WxReplySelect :objData="objData" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -183,7 +183,7 @@ 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 WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 import * as MpAutoReplyApi from '@/api/mp/autoReply'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
@@ -195,7 +195,7 @@ const message = useMessage()
 const formRef = ref()
 
 // 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复)
-// 作为tab.name
+// 作为tab.name,enum的数字不能随意修改,与api参数相关
 enum MsgType {
   Follow = 1,
   Message = 2,
@@ -277,9 +277,6 @@ const rules = {
   requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }]
 }
 
-// 重置 WxReplySelect 组件,解决无法清除的问题
-const hackResetWxReplySelect = ref(false)
-
 const onAccountChanged = (id?: number) => {
   queryParams.accountId = id
   getList()
@@ -314,7 +311,6 @@ const handleTabChange = (tabName: TabPaneName) => {
 /** 新增按钮操作 */
 const handleAdd = () => {
   reset()
-  resetEditor()
   // 打开表单,并设置初始化
   objData.value = {
     type: 'text',
@@ -328,7 +324,6 @@ const handleAdd = () => {
 /** 修改按钮操作 */
 const handleUpdate = async (row: any) => {
   reset()
-  resetEditor()
 
   const data = await MpAutoReplyApi.getAutoReply(row.id)
   // 设置属性
@@ -386,7 +381,7 @@ const handleSubmit = async () => {
   }
 
   showReplyFormDialog.value = false
-  getList()
+  await getList()
 }
 
 // 表单重置
@@ -408,14 +403,6 @@ const cancel = () => {
   reset()
 }
 
-// 表单 Editor 重置
-const resetEditor = () => {
-  hackResetWxReplySelect.value = false // 销毁组件
-  nextTick(() => {
-    hackResetWxReplySelect.value = true // 重建组件
-  })
-}
-
 const handleDelete = async (row) => {
   await message.confirm('是否确认删除此数据?')
   await MpAutoReplyApi.deleteAutoReply(row.id)

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

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

+ 21 - 28
src/views/mp/components/wx-account-select/main.vue

@@ -1,44 +1,37 @@
 <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>
+  <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>
 
-<script setup name="WxAccountSelect">
+<script lang="ts" 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 account: MpAccountApi.AccountVO = reactive({
+  id: undefined,
+  name: ''
 })
+const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
+
+const emit = defineEmits<{
+  (e: 'change', id?: number, name?: string): void
+}>()
 
 const handleQuery = async () => {
   accountList.value = await MpAccountApi.getSimpleAccountList()
   // 默认选中第一个
   if (accountList.value.length > 0) {
-    accountId.value = accountList.value[0].id
-    emit('change', accountId.value)
+    account.id = accountList.value[0].id
+    emit('change', account.id, account.name)
   }
 }
 
-const accountChanged = () => {
-  emit('change', accountId.value)
+const onChanged = () => {
+  emit('change', account.id, account.name)
 }
+
+/** 初始化 */
+onMounted(() => {
+  handleQuery()
+})
 </script>

+ 0 - 104
src/views/mp/components/wx-editor/WxEditor.vue

@@ -1,104 +0,0 @@
-<script name="WxEditor" setup>
-import { reactive, ref } from 'vue'
-import { getAccessToken } from '@/utils/auth'
-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()
-
-const props = defineProps({
-  /* 公众号账号编号 */
-  accountId: {
-    type: Number,
-    required: true
-  },
-  /* 编辑器的内容 */
-  value: {
-    type: String,
-    default: ''
-  },
-  /* 图片大小 */
-  maxSize: {
-    type: Number,
-    default: 4000 // kb
-  }
-})
-
-const emit = defineEmits(['input'])
-
-const myQuillEditorRef = ref()
-const content = ref(props.value.replace(/data-src/g, 'src'))
-const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
-const uploadData = reactive({
-  type: 'image', // TODO 芋艿:试试要不要换成 thumb
-  accountId: props.accountId
-})
-
-const onEditorChange = (text) => {
-  //内容改变事件
-  emit('input', text)
-}
-
-// 富文本图片上传前
-const beforeUpload = () => {
-  // 显示 loading 动画
-  loading.value = true
-}
-
-// 图片上传成功
-// 注意!由于微信公众号的图片有访问限制,所以会显示“此图片来自微信公众号,未经允许不可引用”
-const uploadSuccess = (res) => {
-  // res为图片服务器返回的数据
-  // 获取富文本组件实例
-  const quill = myQuillEditorRef.value.quill
-  // 如果上传成功
-  const link = res.data
-  if (link) {
-    // 获取光标所在位置
-    let length = quill.getSelection().index
-    // 插入图片  res.info为服务器返回的图片地址
-    quill.insertEmbed(length, 'image', link)
-    // 调整光标到最后
-    quill.setSelection(length + 1)
-  } else {
-    message.error('图片插入失败')
-  }
-  // loading 动画消失
-  loading.value = false
-}
-
-// 富文本图片上传失败
-const uploadError = () => {
-  // loading 动画消失
-  loading.value = false
-  message.error('图片插入失败')
-}
-</script>
-
-<template>
-  <div id="wxEditor">
-    <div v-loading="loading" element-loading-text="请稍等,图片上传中">
-      <!-- 图片上传组件辅助-->
-      <el-upload
-        :action="actionUrl"
-        :before-upload="beforeUpload"
-        :data="uploadData"
-        :headers="headers"
-        :on-error="uploadError"
-        :on-success="uploadSuccess"
-        :show-file-list="false"
-        class="avatar-uploader"
-        name="file"
-      />
-      <Editor
-        ref="quillEditorRef"
-        :modelValue="content"
-        editor-id="wxEditor"
-        @change="(editor_) => onEditorChange(editor_.getText())"
-      />
-    </div>
-  </div>
-</template>

+ 0 - 45
src/views/mp/components/wx-editor/quill-options.js

@@ -1,45 +0,0 @@
-const toolbarOptions = [
-  ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
-  ['blockquote', 'code-block'], // 引用  代码块
-  [{ header: 1 }, { header: 2 }], // 1、2 级标题
-  [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
-  [{ script: 'sub' }, { script: 'super' }], // 上标/下标
-  [{ indent: '-1' }, { indent: '+1' }], // 缩进
-  // [{'direction': 'rtl'}],                         // 文本方向
-  [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
-  [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
-  [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
-  [{ font: [] }], // 字体种类
-  [{ align: [] }], // 对齐方式
-  ['clean'], // 清除文本格式
-  ['link', 'image', 'video'] // 链接、图片、视频
-]
-
-export default {
-  theme: 'snow',
-  placeholder: '请输入文章内容',
-  modules: {
-    toolbar: {
-      container: toolbarOptions,
-      // container: "#toolbar",
-      handlers: {
-        image: function (value) {
-          if (value) {
-            // 触发input框选择图片文件
-            document.querySelector('.avatar-uploader input').click()
-          } else {
-            this.quill.format('image', false)
-          }
-        },
-        link: function (value) {
-          if (value) {
-            const href = prompt('注意!只支持公众号图文链接')
-            this.quill.format('link', href)
-          } else {
-            this.quill.format('link', false)
-          }
-        }
-      }
-    }
-  }
-}

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

@@ -231,7 +231,7 @@
                     :on-success="handleUploadSuccess"
                   >
                     <template #trigger>
-                      <el-button type="text">本地上传</el-button>
+                      <el-button type="primary" link>本地上传</el-button>
                     </template>
                     <el-button type="primary" link @click="openMaterial" style="margin-left: 5px"
                       >素材库选择

+ 75 - 0
src/views/mp/draft/editor-config.ts

@@ -0,0 +1,75 @@
+import { IEditorConfig } from '@wangeditor/editor'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+
+const message = useMessage()
+
+type InsertFnType = (url: string, alt: string, href: string) => void
+
+export const createEditorConfig = (
+  server: string,
+  accountId: number | undefined
+): Partial<IEditorConfig> => {
+  return {
+    MENU_CONF: {
+      ['uploadImage']: {
+        server,
+        // 单个文件的最大体积限制,默认为 2M
+        maxFileSize: 5 * 1024 * 1024,
+        // 最多可上传几个文件,默认为 100
+        maxNumberOfFiles: 10,
+        // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
+        allowedFileTypes: ['image/*'],
+
+        // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
+        meta: {
+          accountId: accountId,
+          type: 'image'
+        },
+        // 将 meta 拼接到 url 参数中,默认 false
+        metaWithUrl: true,
+
+        // 自定义增加 http  header
+        headers: {
+          Accept: '*',
+          Authorization: 'Bearer ' + getAccessToken(),
+          'tenant-id': getTenantId()
+        },
+
+        // 跨域是否传递 cookie ,默认为 false
+        withCredentials: true,
+
+        // 超时时间,默认为 10 秒
+        timeout: 5 * 1000, // 5 秒
+
+        // form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
+        fieldName: 'file',
+
+        // 上传之前触发
+        onBeforeUpload(file: File) {
+          console.log(file)
+          return file
+        },
+        // 上传进度的回调函数
+        onProgress(progress: number) {
+          // progress 是 0-100 的数字
+          console.log('progress', progress)
+        },
+        onSuccess(file: File, res: any) {
+          console.log('onSuccess', file, res)
+        },
+        onFailed(file: File, res: any) {
+          message.alertError(res.message)
+          console.log('onFailed', file, res)
+        },
+        onError(file: File, err: any, res: any) {
+          message.alertError(err.message)
+          console.error('onError', file, err, res)
+        },
+        // 自定义插入图片
+        customInsert(res: any, insertFn: InsertFnType) {
+          insertFn(res.data.url, 'image', res.data.url)
+        }
+      }
+    }
+  }
+}

+ 237 - 230
src/views/mp/draft/index.vue

@@ -11,7 +11,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item>
         <el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']">
@@ -71,168 +71,182 @@
     <el-dialog
       :title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
       width="80%"
-      top="20px"
       v-model="dialogNewsVisible"
       :before-close="dialogNewsClose"
-      :close-on-click-modal="false"
+      destroy-on-close
     >
-      <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>
-            </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%"
-                  />
+      <el-container>
+        <el-aside width="40%">
+          <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>
               </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="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"
+                      width="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>
             </div>
-          </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>
-      <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"
+            <el-row justify="center" class="ope-row">
+              <el-button
+                type="primary"
+                circle
+                @click="plusNews"
+                v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
               >
-                <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>
-            <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%" append-to-body>
-              <WxMaterialSelect
-                ref="materialSelectRef"
-                :objData="{ type: 'image', accountId: queryParams.accountId }"
-                @select-material="selectMaterial"
+                <Icon icon="ep:plus" />
+              </el-button>
+            </el-row>
+          </div>
+        </el-aside>
+        <el-main>
+          <div class="" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
+            <!-- 标题、作者、原文地址 -->
+            <el-row :gutter="20">
+              <el-input
+                v-model="articlesAdd[isActiveAddNews].title"
+                placeholder="请输入标题(必填)"
+              />
+              <el-input
+                v-model="articlesAdd[isActiveAddNews].author"
+                placeholder="请输入作者"
+                style="margin-top: 5px"
               />
-            </el-dialog>
+              <el-input
+                v-model="articlesAdd[isActiveAddNews].contentSourceUrl"
+                placeholder="请输入原文地址"
+                style="margin-top: 5px"
+              />
+            </el-row>
+            <!-- 封面和摘要 -->
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <p>封面:</p>
+                <div class="thumb-div">
+                  <el-image
+                    v-if="articlesAdd[isActiveAddNews].thumbUrl"
+                    style="width: 300px; max-height: 300px"
+                    :src="articlesAdd[isActiveAddNews].thumbUrl"
+                    fit="contain"
+                  />
+                  <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 #tip>
+                        <div class="el-upload__tip"
+                          >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div
+                        >
+                      </template>
+                    </el-upload>
+                  </div>
+                  <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-col>
+              <el-col :span="12">
+                <p>摘要:</p>
+                <el-input
+                  :rows="8"
+                  type="textarea"
+                  v-model="articlesAdd[isActiveAddNews].digest"
+                  placeholder="请输入摘要"
+                  class="digest"
+                  maxlength="120"
+                />
+              </el-col>
+            </el-row>
+            <!--富文本编辑器组件-->
+            <el-row>
+              <Editor
+                v-model="articlesAdd[isActiveAddNews].content"
+                :editor-config="editorConfig"
+              />
+            </el-row>
           </div>
-          <el-input
-            :rows="8"
-            type="textarea"
-            v-model="articlesAdd[isActiveAddNews].digest"
-            placeholder="请输入摘要"
-            class="digest"
-            maxlength="120"
-            style="float: right"
-          />
-        </div>
-        <!--富文本编辑器组件-->
-        <el-row>
-          <WxEditor
-            v-model="articlesAdd[isActiveAddNews].content"
-            :account-id="uploadData.accountId"
-            v-if="hackResetEditor"
-          />
-        </el-row>
-      </div>
+        </el-main>
+      </el-container>
       <template #footer>
         <el-button @click="dialogNewsVisible = false">取 消</el-button>
         <el-button type="primary" @click="submitForm">提 交</el-button>
@@ -242,16 +256,18 @@
 </template>
 
 <script setup lang="ts" name="MpDraft">
-import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue'
+import { Editor } from '@/components/Editor'
 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 WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 import { getAccessToken } from '@/utils/auth'
 import * as MpDraftApi from '@/api/mp/draft'
 import * as MpFreePublishApi from '@/api/mp/freePublish'
 import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
-// 可以用改本地数据模拟,避免API调用超限
-// import drafts from './mock'
+import { createEditorConfig } from './editor-config'
+// import drafts from './mock' // 可以用改本地数据模拟,避免API调用超限
+import { IEditorConfig } from '@wangeditor/editor'
+
 const message = useMessage() // 消息
 
 const loading = ref(true) // 列表的加载中
@@ -285,14 +301,25 @@ const uploadData: UploadData = reactive({
 })
 
 // ========== 草稿新建 or 修改 ==========
+interface Article {
+  title: string
+  thumbMediaId: string
+  author: string
+  digest: string
+  showCoverPic: string
+  content: string
+  contentSourceUrl: string
+  needOpenComment: string
+  onlyFansCanComment: string
+  thumbUrl: string
+}
 const dialogNewsVisible = ref(false)
 const addMaterialLoading = ref(false) // 添加草稿的 loading 标识
-const articlesAdd = ref<any[]>([])
+const articlesAdd = ref<Article[]>([])
 const isActiveAddNews = ref(0)
 const dialogImageVisible = ref(false)
 const operateMaterial = ref<'add' | 'edit'>('add')
 const articlesMediaId = ref('')
-const hackResetEditor = ref(false)
 
 /** 侦听公众号变化 **/
 const onAccountChanged = (id?: number) => {
@@ -305,8 +332,11 @@ const onAccountChanged = (id?: number) => {
 const setAccountId = (id?: number) => {
   queryParams.accountId = id
   uploadData.accountId = id
+  editorConfig.value = createEditorConfig(uploadUrl, queryParams.accountId)
 }
 
+const editorConfig = ref<Partial<IEditorConfig>>({})
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -329,7 +359,6 @@ const getList = async () => {
 // ======================== 新增/修改草稿 ========================
 /** 新增按钮操作 */
 const handleAdd = () => {
-  resetEditor()
   reset()
   // 打开表单,并设置初始化
   operateMaterial.value = 'add'
@@ -338,7 +367,6 @@ const handleAdd = () => {
 
 /** 更新按钮操作 */
 const handleUpdate = (item: any) => {
-  resetEditor()
   reset()
   articlesMediaId.value = item.mediaId
   articlesAdd.value = JSON.parse(JSON.stringify(item.content.newsItem))
@@ -370,7 +398,6 @@ const dialogNewsClose = async (onDone: () => {}) => {
   try {
     await message.confirm('修改内容可能还未保存,确定关闭吗?')
     reset()
-    resetEditor()
     onDone()
   } catch {}
 }
@@ -381,14 +408,6 @@ const reset = () => {
   articlesAdd.value = [buildEmptyArticle()]
 }
 
-// 表单 Editor 重置
-const resetEditor = () => {
-  hackResetEditor.value = false // 销毁组件
-  nextTick(() => {
-    hackResetEditor.value = true // 重建组件
-  })
-}
-
 // 将图文向下移动
 const downNews = (index: number) => {
   let temp = articlesAdd.value[index]
@@ -398,8 +417,8 @@ const downNews = (index: number) => {
 }
 
 // 将图文向上移动
-const upNews = (index) => {
-  let temp = articlesAdd.value[index]
+const upNews = (index: number) => {
+  const temp = articlesAdd.value[index]
   articlesAdd.value[index] = articlesAdd.value[index - 1]
   articlesAdd.value[index - 1] = temp
   isActiveAddNews.value = index - 1
@@ -407,7 +426,6 @@ const upNews = (index) => {
 
 // 选中指定 index 的图文
 const activeNews = (index: number) => {
-  resetEditor()
   isActiveAddNews.value = index
 }
 
@@ -429,7 +447,7 @@ const plusNews = () => {
 }
 
 // 创建空的 article
-const buildEmptyArticle = () => {
+const buildEmptyArticle = (): Article => {
   return {
     title: '',
     thumbMediaId: '',
@@ -455,8 +473,8 @@ const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawF
     addMaterialLoading.value = false
     return false
   }
-  const isLt = rawFile.size / 1024 / 1024 < 2
-  if (!isLt) {
+
+  if (rawFile.size / 1024 / 1024 > 2) {
     message.error('上传图片大小不能超过 2M!')
     addMaterialLoading.value = false
     return false
@@ -465,10 +483,10 @@ const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawF
   return true
 }
 
-const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
+const handleUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
   addMaterialLoading.value = false
-  if (response.code !== 0) {
-    message.error('上传出错:' + response.msg)
+  if (res.code !== 0) {
+    message.error('上传出错:' + res.msg)
     return false
   }
 
@@ -476,8 +494,8 @@ const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
   fileList.value = []
 
   // 设置草稿的封面字段
-  articlesAdd.value[isActiveAddNews.value].thumbMediaId = response.data.mediaId
-  articlesAdd.value[isActiveAddNews.value].thumbUrl = response.data.url
+  articlesAdd.value[isActiveAddNews.value].thumbMediaId = res.data.mediaId
+  articlesAdd.value[isActiveAddNews.value].thumbUrl = res.data.url
 }
 
 // 选择 or 上传完素材,设置回草稿
@@ -502,7 +520,7 @@ const handlePublish = async (item: any) => {
     await message.confirm(content)
     await MpFreePublishApi.submitFreePublish(accountId, mediaId)
     message.notifySuccess('发布成功')
-    getList()
+    await getList()
   } catch {}
 }
 
@@ -514,7 +532,7 @@ const handleDelete = async (item: any) => {
     await message.confirm('此操作将永久删除该草稿, 是否继续?')
     await MpDraftApi.deleteDraft(accountId, mediaId)
     message.notifySuccess('删除成功')
-    getList()
+    await getList()
   } catch {}
 }
 </script>
@@ -535,6 +553,13 @@ const handleDelete = async (item: any) => {
   padding-top: 5px;
 }
 
+.el-row {
+  margin-bottom: 20px;
+}
+.el-row:last-child {
+  margin-bottom: 0;
+}
+
 .item-name {
   font-size: 12px;
   overflow: hidden;
@@ -548,35 +573,33 @@ const handleDelete = async (item: any) => {
 }
 
 /*新增图文*/
-.left {
-  display: inline-block;
-  width: 35%;
-  vertical-align: top;
-  margin-top: 200px;
-}
-
-.right {
-  display: inline-block;
-  width: 60%;
-  margin-top: -40px;
-}
-
-.avatar-uploader {
-  width: 20%;
-  display: inline-block;
-}
-
-.avatar-uploader .el-upload {
-  border-radius: 6px;
-  cursor: pointer;
-  position: relative;
-  overflow: hidden;
-  text-align: unset !important;
-}
-
-.avatar-uploader .el-upload:hover {
-  border-color: #165dff;
-}
+// .left {
+//   display: inline-block;
+//   vertical-align: top;
+//   margin-top: 200px;
+// }
+
+// .right {
+//   display: inline-block;
+//   width: 100%;
+// }
+
+// .avatar-uploader {
+//   width: 20%;
+//   display: inline-block;
+// }
+
+// .avatar-uploader .el-upload {
+//   border-radius: 6px;
+//   cursor: pointer;
+//   position: relative;
+//   overflow: hidden;
+//   text-align: unset !important;
+// }
+
+// .avatar-uploader .el-upload:hover {
+//   border-color: #165dff;
+// }
 
 .avatar-uploader-icon {
   border: 1px solid #d9d9d9;
@@ -599,7 +622,7 @@ const handleDelete = async (item: any) => {
 }
 
 .digest {
-  width: 60%;
+  width: 100%;
   display: inline-block;
   vertical-align: top;
 }
@@ -620,28 +643,16 @@ const handleDelete = async (item: any) => {
   border: 1px solid #eaeaea;
 }
 
-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) {
@@ -707,10 +718,6 @@ p {
   background-color: #acadae;
 }
 
-.input-tt {
-  padding: 5px;
-}
-
 .activeAddNews {
   border: 5px solid #2bb673;
 }
@@ -747,7 +754,7 @@ p {
 
 .thumb-div {
   display: inline-block;
-  width: 30%;
+  width: 100%;
   text-align: center;
 }
 

+ 2 - 4
src/views/mp/freePublish/index.vue

@@ -11,7 +11,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -51,8 +51,7 @@
 <script lang="ts" setup name="MpFreePublish">
 import * as FreePublishApi from '@/api/mp/freePublish'
 import WxNews from '@/views/mp/components/wx-news/main.vue'
-import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
-
+import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -102,7 +101,6 @@ const handleDelete = async (item: any) => {
   } catch {}
 }
 </script>
-
 <style lang="scss" scoped>
 .ope-row {
   margin-top: 5px;

+ 2 - 2
src/views/mp/material/index.vue

@@ -4,7 +4,7 @@
   <ContentWrap>
     <el-form class="-mb-15px" :inline="true" label-width="68px">
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -248,7 +248,7 @@
 <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 WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 import * as MpMaterialApi from '@/api/mp/material'
 import * as authUtil from '@/utils/auth'
 import { dateFormatter } from '@/utils/formatTime'

+ 2 - 2
src/views/mp/menu/index.vue

@@ -4,7 +4,7 @@
   <ContentWrap>
     <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -192,7 +192,7 @@
 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 WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 import * as MpMenuApi from '@/api/mp/menu'
 import { handleTree } from '@/utils/tree'
 import menuOptions from './menuOptions'

+ 145 - 0
src/views/mp/message/MessageTable.vue

@@ -0,0 +1,145 @@
+<template>
+  <div>
+    <el-table v-loading="props.loading" :data="props.list">
+      <el-table-column
+        label="发送时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="消息类型" align="center" prop="type" width="80" />
+      <el-table-column label="发送方" align="center" prop="sendFrom" width="80">
+        <template #default="scope">
+          <el-tag v-if="scope.row.sendFrom === 1" type="success">粉丝</el-tag>
+          <el-tag v-else type="info">公众号</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="用户标识" align="center" prop="openid" width="300" />
+      <el-table-column label="内容" prop="content">
+        <template #default="scope">
+          <!-- 【事件】区域 -->
+          <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 === MsgType.Event && scope.row.event === 'unsubscribe'">
+            <el-tag type="danger">取消关注</el-tag>
+          </div>
+          <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 === MsgType.Event && scope.row.event === 'VIEW'">
+            <el-tag>点击菜单链接</el-tag>
+            【{{ scope.row.eventKey }}】
+          </div>
+          <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 === MsgType.Event && scope.row.event === 'scancode_push'">
+            <el-tag>扫码结果</el-tag>
+            【{{ scope.row.eventKey }}】
+          </div>
+          <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 === MsgType.Event && scope.row.event === 'pic_photo_or_album'"
+          >
+            <el-tag>拍照或者相册</el-tag>
+          </div>
+          <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 === MsgType.Event && scope.row.event === 'location_select'"
+          >
+            <el-tag>选择地理位置</el-tag>
+          </div>
+          <div v-else-if="scope.row.type === MsgType.Event">
+            <el-tag type="danger">未知事件类型</el-tag>
+          </div>
+          <!-- 【消息】区域 -->
+          <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 === 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 === 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 === 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 === 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 === MsgType.Music">
+            <WxMusic
+              :title="scope.row.title"
+              :description="scope.row.description"
+              :thumb-media-url="scope.row.thumbMediaUrl"
+              :music-url="scope.row.musicUrl"
+              :hq-music-url="scope.row.hqMusicUrl"
+            />
+          </div>
+          <div v-else-if="scope.row.type === MsgType.News">
+            <WxNews :articles="scope.row.articles" />
+          </div>
+          <div v-else>
+            <el-tag type="danger">未知消息类型</el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="emit('send', scope.row.userId)"
+            v-hasPermi="['mp:message:send']"
+          >
+            消息
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+  </div>
+</template>
+
+<script setup lang="ts">
+import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play/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 { dateFormatter } from '@/utils/formatTime'
+import { MsgType } from '@/views/mp/components/wx-msg/types'
+
+const props = defineProps({
+  list: {
+    type: Array,
+    required: true
+  },
+  loading: {
+    type: Boolean,
+    required: true
+  }
+})
+
+const emit = defineEmits<{ (e: 'send', v: number) }>()
+</script>

+ 34 - 153
src/views/mp/message/index.vue

@@ -9,7 +9,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item label="消息类型" prop="type">
         <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px">
@@ -58,124 +58,7 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list">
-      <el-table-column
-        label="发送时间"
-        align="center"
-        prop="createTime"
-        width="180"
-        :formatter="dateFormatter"
-      />
-      <el-table-column label="消息类型" align="center" prop="type" width="80" />
-      <el-table-column label="发送方" align="center" prop="sendFrom" width="80">
-        <template #default="scope">
-          <el-tag v-if="scope.row.sendFrom === 1" type="success">粉丝</el-tag>
-          <el-tag v-else type="info">公众号</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="用户标识" align="center" prop="openid" width="300" />
-      <el-table-column label="内容" prop="content">
-        <template #default="scope">
-          <!-- 【事件】区域 -->
-          <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 === MsgType.Event && scope.row.event === 'unsubscribe'">
-            <el-tag type="danger">取消关注</el-tag>
-          </div>
-          <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 === MsgType.Event && scope.row.event === 'VIEW'">
-            <el-tag>点击菜单链接</el-tag>
-            【{{ scope.row.eventKey }}】
-          </div>
-          <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 === MsgType.Event && scope.row.event === 'scancode_push'">
-            <el-tag>扫码结果</el-tag>
-            【{{ scope.row.eventKey }}】
-          </div>
-          <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 === MsgType.Event && scope.row.event === 'pic_photo_or_album'"
-          >
-            <el-tag>拍照或者相册</el-tag>
-          </div>
-          <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 === MsgType.Event && scope.row.event === 'location_select'"
-          >
-            <el-tag>选择地理位置</el-tag>
-          </div>
-          <div v-else-if="scope.row.type === MsgType.Event">
-            <el-tag type="danger">未知事件类型</el-tag>
-          </div>
-          <!-- 【消息】区域 -->
-          <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 === 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 === 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 === 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 === 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 === MsgType.Music">
-            <WxMusic
-              :title="scope.row.title"
-              :description="scope.row.description"
-              :thumb-media-url="scope.row.thumbMediaUrl"
-              :music-url="scope.row.musicUrl"
-              :hq-music-url="scope.row.hqMusicUrl"
-            />
-          </div>
-          <div v-else-if="scope.row.type === MsgType.News">
-            <WxNews :articles="scope.row.articles" />
-          </div>
-          <div v-else>
-            <el-tag type="danger">未知消息类型</el-tag>
-          </div>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="handleSend(scope.row)"
-            v-hasPermi="['mp:message:send']"
-          >
-            消息
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页组件 -->
+    <MessageTable :list="list" :loading="loading" @send="handleSend" />
     <Pagination
       v-show="total > 0"
       :total="total"
@@ -183,37 +66,31 @@
       v-model:limit="queryParams.pageSize"
       @pagination="getList"
     />
-
-    <!-- 发送消息的弹窗 -->
-    <el-dialog
-      title="粉丝消息列表"
-      v-model="showMessageBox"
-      @click="showMessageBox = true"
-      width="50%"
-    >
-      <template #footer>
-        <WxMsg :user-id="userId" v-if="showMessageBox" />
-      </template>
-    </el-dialog>
   </ContentWrap>
+
+  <!-- 发送消息的弹窗 -->
+  <el-dialog
+    title="粉丝消息列表"
+    v-model="messageBox.show"
+    @click="messageBox.show = true"
+    width="50%"
+    destroy-on-close
+  >
+    <WxMsg :user-id="messageBox.userId" />
+  </el-dialog>
 </template>
 <script setup lang="ts" name="MpMessage">
-import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
-import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
-import WxMsg from '@/views/mp/components/wx-msg/main.vue'
-import 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 WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 import * as MpMessageApi from '@/api/mp/message'
+import WxMsg from '@/views/mp/components/wx-msg/main.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
+import MessageTable from './MessageTable.vue'
 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<any[]>([]) // 列表数据
+const loading = ref(false)
+const total = ref(0) // 数据的总页数
+const list = ref<any[]>([]) // 当前页的列表数据
 
 // 搜索参数
 interface QueryParams {
@@ -233,8 +110,12 @@ const queryParams: QueryParams = reactive({
   createTime: []
 })
 const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
-const showMessageBox = ref(false) // 是否显示弹出层
-const userId = ref(0) // 操作的用户编号
+
+// 消息对话框
+const messageBox = reactive({
+  show: false,
+  userId: 0
+})
 
 /** 侦听accountId */
 const onAccountChanged = (id?: number) => {
@@ -243,6 +124,11 @@ const onAccountChanged = (id?: number) => {
 }
 
 /** 查询列表 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
 const getList = async () => {
   try {
     loading.value = true
@@ -254,14 +140,9 @@ const getList = async () => {
   }
 }
 
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
 /** 重置按钮操作 */
 const resetQuery = async () => {
+  // 暂存 accountId,并在 reset 后恢复
   const accountId = queryParams.accountId
   queryFormRef.value?.resetFields()
   queryParams.accountId = accountId
@@ -269,8 +150,8 @@ const resetQuery = async () => {
 }
 
 /** 打开消息发送窗口 */
-const handleSend = async (row: any) => {
-  userId.value = row.userId
-  showMessageBox.value = true
+const handleSend = async (userId: number) => {
+  messageBox.userId = userId
+  messageBox.show = true
 }
 </script>

+ 2 - 2
src/views/mp/tag/index.vue

@@ -11,7 +11,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item>
         <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']">
@@ -74,7 +74,7 @@
 import { dateFormatter } from '@/utils/formatTime'
 import * as MpTagApi from '@/api/mp/tag'
 import TagForm from './TagForm.vue'
-import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 

+ 2 - 2
src/views/mp/user/index.vue

@@ -11,7 +11,7 @@
       label-width="68px"
     >
       <el-form-item label="公众号" prop="accountId">
-        <WxMpSelect @change="onAccountChanged" />
+        <WxAccountSelect @change="onAccountChanged" />
       </el-form-item>
       <el-form-item label="用户标识" prop="openid">
         <el-input
@@ -97,7 +97,7 @@
 import { dateFormatter } from '@/utils/formatTime'
 import * as MpUserApi from '@/api/mp/user'
 import * as MpTagApi from '@/api/mp/tag'
-import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 import type { FormInstance } from 'element-plus'
 import UserForm from './UserForm.vue'
 

+ 2 - 1
src/views/system/dict/DictTypeForm.vue

@@ -59,7 +59,8 @@ const formData = ref({
 })
 const formRules = reactive({
   name: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
-  type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }]
+  type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
 })
 const formRef = ref() // 表单 Ref
 

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

@@ -83,7 +83,8 @@ const formData = ref({
 const formRules = reactive({
   label: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
   value: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
-  sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }]
+  sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
 })
 const formRef = ref() // 表单 Ref
 

+ 3 - 1
src/views/system/tenant/TenantForm.vue

@@ -105,7 +105,9 @@ const formRules = reactive({
   status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
   accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
   expireTime: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }],
-  domain: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }]
+  domain: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }],
+  username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
+  password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const packageList = ref([]) // 租户套餐