Commit 60cc4357 by 芋道源码 Committed by Gitee

!123 【众测版】合并最新的 Vue3 重构

Merge pull request !123 from 芋道源码/dev
parents 1170a948 f1f49fdf
......@@ -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"
}
}
......@@ -75,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']
......
......@@ -32,7 +32,6 @@
"@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",
......
......@@ -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
......
......@@ -95,7 +95,7 @@ watch(
return props.modelValue
},
() => {
if (props.modelValue && props.modelValue.contains(':')) {
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)
}
......
......@@ -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
......
/* 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']
}
}
/* 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'
}
......@@ -18,7 +18,8 @@
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item prop="tenantName" v-if="loginData.tenantEnable === 'true'">
<el-input
type="text"
type="primary"
link
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
......
......@@ -19,7 +19,8 @@
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item prop="tenantName" v-if="loginData.tenantEnable === 'true'">
<el-input
type="text"
type="primary"
link
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
......
......@@ -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>
......
......@@ -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>
......
<template>
<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>
</template>
<script lang="ts" setup name="WxMpSelect">
import * as MpAccountApi from '@/api/mp/account'
const accountId: Ref<number | undefined> = ref()
const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
const emit = defineEmits<{
(e: 'change', id: number | undefined): void
}>()
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>
<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>
<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'
const BASE_URL = import.meta.env.VITE_BASE_URL
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 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 = () => {
//内容改变事件
emit('input', content.value)
}
// 富文本图片上传前
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
class="avatar-uploader"
name="file"
:action="actionUrl"
:headers="headers"
:show-file-list="false"
:data="uploadData"
:on-success="uploadSuccess"
:on-error="uploadError"
:before-upload="beforeUpload"
/>
<QuillEditor
class="editor"
v-model="content"
ref="quillEditorRef"
:options="editorOptions"
@change="onEditorChange($event)"
/>
</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>
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)
}
}
}
}
}
}
......@@ -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: {
......
export enum MsgType {
Event = 'event',
Text = 'text',
Voice = 'voice',
Image = 'image',
Video = 'video',
Link = 'link',
Location = 'location',
Music = 'music',
News = 'news'
}
......@@ -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"
>素材库选择
......
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)
}
}
}
}
}
......@@ -3,8 +3,17 @@
<!-- 搜索工作栏 -->
<ContentWrap>
<!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect -->
<WxAccountSelect @change="(accountId) => accountChanged(accountId)" />
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="公众号" prop="accountId">
<WxAccountSelect @change="onAccountChanged" />
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
......@@ -39,26 +48,31 @@
</ContentWrap>
</template>
<script setup name="MpFreePublish">
<script lang="ts" setup name="MpFreePublish">
import * as FreePublishApi from '@/api/mp/freePublish'
import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.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
})
/** 侦听公众号变化 **/
const accountChanged = (accountId) => {
queryParams.accountId = accountId
const onAccountChanged = (id: number | undefined) => {
queryParams.accountId = id
getList()
}
......@@ -75,7 +89,7 @@ const getList = async () => {
}
/** 删除按钮操作 */
const handleDelete = async (item) => {
const handleDelete = async (item: any) => {
try {
// 删除的二次确认
await message.delConfirm('删除后用户将无法访问此页面,确定删除?')
......@@ -87,7 +101,6 @@ const handleDelete = async (item) => {
} catch {}
}
</script>
<style lang="scss" scoped>
.ope-row {
margin-top: 5px;
......
......@@ -4,13 +4,13 @@
<ContentWrap>
<el-form class="-mb-15px" :inline="true" label-width="68px">
<el-form-item label="公众号" prop="accountId">
<WxMpSelect @change="accountChange" />
<WxAccountSelect @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>
......@@ -93,7 +93,7 @@
<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
......@@ -188,10 +188,8 @@
</el-row>
</el-form>
<template #footer>
<!-- <span class="dialog-footer"> -->
<el-button @click="cancelVideo">取 消</el-button>
<el-button type="primary" @click="submitVideo">提 交</el-button>
<!-- </span> -->
</template>
</el-dialog>
......@@ -203,7 +201,7 @@
<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
......@@ -250,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'
......@@ -279,14 +277,11 @@ const uploadRules: FormRules = {
}
// 素材类型
type MatertialType = 'image' | 'voice' | 'video'
const type = ref<MatertialType>('image')
// 遮罩层
const loading = ref(false)
// 总条数
const total = ref(0)
// 数据列表
const list = ref([])
type MaterialType = 'image' | 'voice' | 'video'
const type = ref<MaterialType>('image')
const loading = ref(false) // 遮罩层
const list = ref<any[]>([]) // 总条数
const total = ref(0) // 数据列表
// 查询参数
interface QueryParams {
pageNo: number
......@@ -304,7 +299,7 @@ const queryParams: QueryParams = reactive({
const fileList = ref<UploadUserFile[]>([])
interface UploadData {
type: MatertialType
type: MaterialType
title: string
introduction: string
}
......@@ -319,8 +314,8 @@ const dialogVideoVisible = ref(false)
const addMaterialLoading = ref(false)
/** 侦听公众号变化 **/
const accountChange = (accountId: number | undefined) => {
queryParams.accountId = accountId
const onAccountChanged = (id?: number) => {
queryParams.accountId = id
getList()
}
......@@ -346,9 +341,9 @@ const handleQuery = () => {
getList()
}
const handleTabChange = (tabName: TabPaneName) => {
const onTabChange = (tabName: TabPaneName) => {
// 设置 type
uploadData.type = tabName as MatertialType
uploadData.type = tabName as MaterialType
// 提前情况数据,避免tab切换后显示垃圾数据
list.value = []
......@@ -359,54 +354,49 @@ const handleTabChange = (tabName: TabPaneName) => {
}
// ======================== 文件上传 ========================
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
rawFile.type
)
if (!isType) {
message.error('上传图片格式不对!')
return false
}
const isLt = rawFile.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: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const isType = ['audio/mp3', 'audio/wma', 'audio/wav', 'audio/amr'].includes(file.type)
const isLt = rawFile.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: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
const isType = rawFile.type === 'video/mp4'
if (!isType) {
message.error('上传视频格式不对!')
return false
}
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
beforeUpload(rawFile, 'image')
const isLt = rawFile.size / 1024 / 1024 < 10
if (!isLt) {
message.error('上传视频大小不能超过 10M!')
return false
}
const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
beforeUpload(rawFile, 'audio')
addMaterialLoading.value = true
return true
}
const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
beforeUpload(rawFile, 'video')
const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
loading.value = false
......@@ -441,6 +431,7 @@ const submitVideo = () => {
})
}
// 弹出 video 新建的表单
const handleAddVideo = () => {
resetVideo()
dialogVideoVisible.value = true
......
......@@ -2,8 +2,11 @@
<doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" />
<!-- 搜索工作栏 -->
<ContentWrap>
<!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect -->
<WxAccountSelect @change="accountChanged" />
<el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="公众号" prop="accountId">
<WxAccountSelect @change="onAccountChanged" />
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
......@@ -12,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">
......@@ -68,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>
......@@ -155,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>
......@@ -185,24 +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 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'
const message = useMessage() // 消息
// ======================== 列表查询 ========================
const loading = ref(true) // 遮罩层
const accountId = ref(undefined) // 公众号Id
const menuList = ref({ children: [] })
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) // 二级菜单显示标志
// ======================== 菜单编辑 ========================
......@@ -210,15 +215,16 @@ 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>({}) // 右边临时变量,作为中间值牵引关系
// 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数
const tempSelfObj = ref({})
const tempSelfObj = ref<any>({})
const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗
/** 侦听公众号变化 **/
const accountChanged = (id) => {
const onAccountChanged = (id?: number, name?: string) => {
accountId.value = id
accountName.value = name
getList()
}
......@@ -241,10 +247,10 @@ const handleQuery = () => {
}
// 将后端返回的 menuList,转换成前端的 menuList
const convertMenuList = (list) => {
const convertMenuList = (list: any[]) => {
if (!list) return []
const menuList = []
const result: any[] = []
list.forEach((item) => {
const menu = {
...item
......@@ -271,9 +277,9 @@ const convertMenuList = (list) => {
hqMusicUrl: item.replyHqMusicUrl
}
}
menuList.push(menu)
result.push(menu)
})
return menuList
return result
}
// 重置表单,清空表单数据
......@@ -286,7 +292,7 @@ const resetForm = () => {
// 菜单编辑
showRightFlag.value = false
nameMaxLength.value = 0
showConfigureContent.value = 0
showConfigureContent.value = false
hackResetWxReplySelect.value = false
tempObj.value = {}
tempSelfObj.value = {}
......@@ -295,7 +301,7 @@ const resetForm = () => {
// ======================== 菜单操作 ========================
// 一级菜单点击事件
const menuClick = (i, item) => {
const menuClick = (i: number, item: any) => {
// 右侧的表单相关
resetEditor()
showRightFlag.value = true // 右边菜单
......@@ -312,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 // 表示一级菜单索引
......@@ -331,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: [],
......@@ -342,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 = []
......@@ -361,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: {
// 用于存储回复内容
......@@ -399,7 +404,7 @@ const handleDeleteMenu = async () => {
// ======================== 菜单编辑 ========================
const handleSave = async () => {
try {
await message.confirm('确定要删除吗?')
await message.confirm('确定要保存吗?')
loading.value = true
await MpMenuApi.saveMenu(accountId.value, convertMenuFormList())
getList()
......@@ -413,7 +418,6 @@ const handleSave = async () => {
const resetEditor = () => {
hackResetWxReplySelect.value = false // 销毁组件
nextTick(() => {
console.log('nextTick')
hackResetWxReplySelect.value = true // 重建组件
})
}
......@@ -432,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)
// 处理子菜单
......@@ -450,7 +454,7 @@ const convertMenuFormList = () => {
}
// 将前端的 menu,转换成后端接收的 menu
const convertMenuForm = (menu) => {
const convertMenuForm = (menu: any) => {
let result = {
...menu,
children: undefined, // 不处理子节点
......@@ -473,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
// 提示,针对多图文
......
<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>
......@@ -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
......
......@@ -3,17 +3,25 @@
<!-- 搜索工作栏 -->
<ContentWrap>
<!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect -->
<WxAccountSelect @change="accountChanged">
<template #actions>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="公众号" prop="accountId">
<WxAccountSelect @change="onAccountChanged" />
</el-form-item>
<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>
<el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']">
<Icon icon="ep:refresh" class="mr-5px" /> 同步
</el-button>
</template>
</WxAccountSelect>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
......@@ -64,25 +72,32 @@
</template>
<script setup lang="ts" name="MpTag">
import { dateFormatter } from '@/utils/formatTime'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
import * as MpTagApi from '@/api/mp/tag'
import TagForm from './TagForm.vue'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.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
})
const formRef = ref<InstanceType<typeof TagForm> | null>(null)
/** 侦听公众号变化 **/
const accountChanged = (accountId) => {
const onAccountChanged = (id?: number) => {
queryParams.pageNo = 1
queryParams.accountId = accountId
queryParams.accountId = id
getList()
}
......@@ -99,9 +114,8 @@ const getList = async () => {
}
/** 添加/修改操作 */
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)
}
/** 删除按钮操作 */
......@@ -121,8 +135,7 @@ 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 {}
......
......@@ -3,14 +3,42 @@
<!-- 搜索工作栏 -->
<ContentWrap>
<!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect -->
<WxAccountSelect @change="(accountId) => accountChanged(accountId)">
<template #actions>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="公众号" prop="accountId">
<WxAccountSelect @change="onAccountChanged" />
</el-form-item>
<el-form-item label="用户标识" prop="openid">
<el-input
v-model="queryParams.openid"
placeholder="请输入用户标识"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input
v-model="queryParams.nickname"
placeholder="请输入昵称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</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="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']">
<Icon icon="ep:refresh" class="mr-5px" /> 同步
</el-button>
</template>
</WxAccountSelect>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
......@@ -66,35 +94,40 @@
<UserForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup name="MpUser">
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
import { dateFormatter } from '@/utils/formatTime'
import * as MpUserApi from '@/api/mp/user'
import * as MpTagApi from '@/api/mp/tag'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.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 tagList = ref([]) // 公众号标签列表
/** 初始化 */
onMounted(async () => {
tagList.value = await MpTagApi.getSimpleTagList()
})
const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
const tagList = ref<any[]>([]) // 公众号标签列表
/** 侦听公众号变化 **/
const accountChanged = (accountId) => {
const onAccountChanged = (id?: number) => {
queryParams.pageNo = 1
queryParams.accountId = accountId
queryParams.accountId = id
getList()
}
......@@ -110,20 +143,38 @@ const getList = async () => {
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
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 {}
}
/** 初始化 */
onMounted(async () => {
tagList.value = await MpTagApi.getSimpleTagList()
})
</script>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment