Commit 02d98f2c by puhui999

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

# Conflicts:
#	src/views/mp/components/wx-editor/WxEditor.vue
#	src/views/mp/tag/TagForm.vue
parents d62e6072 1a1bfe0e
...@@ -75,7 +75,8 @@ const include = [ ...@@ -75,7 +75,8 @@ const include = [
'element-plus/es/components/dropdown-item/style/css', 'element-plus/es/components/dropdown-item/style/css',
'element-plus/es/components/badge/style/css', 'element-plus/es/components/badge/style/css',
'element-plus/es/components/breadcrumb/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'] const exclude = ['@iconify/json']
......
...@@ -95,7 +95,7 @@ watch( ...@@ -95,7 +95,7 @@ watch(
return props.modelValue 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) currentActiveType.value = props.modelValue.substring(0, props.modelValue.indexOf(':') + 1)
icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1) icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1)
} }
......
...@@ -62,6 +62,8 @@ declare global { ...@@ -62,6 +62,8 @@ declare global {
const useRouter: typeof import('vue-router')['useRouter'] const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots'] const useSlots: typeof import('vue')['useSlots']
const useTable: typeof import('@/hooks/web/useTable')['useTable'] 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 watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect'] const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect'] const watchPostEffect: typeof import('vue')['watchPostEffect']
......
<template> <template>
<el-select <el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged">
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-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
</template> </template>
<!-- TODO @芋艿:WxMpSelect 改成 WxAccountSelect,然后挪到现有的 wx-account-select 包下 -->
<script lang="ts" setup name="WxMpSelect"> <script lang="ts" setup name="WxMpSelect">
import * as MpAccountApi from '@/api/mp/account' import * as MpAccountApi from '@/api/mp/account'
const accountId: Ref<number | undefined> = ref() const account: MpAccountApi.AccountVO = reactive({
id: undefined,
name: ''
})
const accountList: Ref<MpAccountApi.AccountVO[]> = ref([]) const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', id: number | undefined): void (e: 'change', id?: number, name?: string): void
}>() }>()
onMounted(() => { onMounted(() => {
...@@ -27,12 +25,12 @@ const handleQuery = async () => { ...@@ -27,12 +25,12 @@ const handleQuery = async () => {
accountList.value = await MpAccountApi.getSimpleAccountList() accountList.value = await MpAccountApi.getSimpleAccountList()
// 默认选中第一个 // 默认选中第一个
if (accountList.value.length > 0) { if (accountList.value.length > 0) {
accountId.value = accountList.value[0].id account.id = accountList.value[0].id
emit('change', accountId.value) emit('change', account.id, account.name)
} }
} }
const accountChanged = () => { const onChanged = () => {
emit('change', accountId.value) emit('change', account.id, account.name)
} }
</script> </script>
<script name="WxEditor" setup> <script name="WxEditor" setup>
import { reactive, ref } from 'vue' 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 { getAccessToken } from '@/utils/auth'
import editorOptions from './quill-options' import { Editor } from '@/components/Editor'
const BASE_URL = import.meta.env.VITE_BASE_URL 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 message = useMessage()
...@@ -30,21 +30,16 @@ const props = defineProps({ ...@@ -30,21 +30,16 @@ const props = defineProps({
const emit = defineEmits(['input']) const emit = defineEmits(['input'])
const myQuillEditorRef = ref() const myQuillEditorRef = ref()
const content = ref(props.value.replace(/data-src/g, 'src')) const content = ref(props.value.replace(/data-src/g, 'src'))
const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示 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({ const uploadData = reactive({
type: 'image', // TODO 芋艿:试试要不要换成 thumb type: 'image', // TODO 芋艿:试试要不要换成 thumb
accountId: props.accountId accountId: props.accountId
}) })
const onEditorChange = () => { const onEditorChange = (text) => {
//内容改变事件 //内容改变事件
emit('input', content.value) emit('input', text)
} }
// 富文本图片上传前 // 富文本图片上传前
...@@ -88,114 +83,22 @@ const uploadError = () => { ...@@ -88,114 +83,22 @@ const uploadError = () => {
<div v-loading="loading" element-loading-text="请稍等,图片上传中"> <div v-loading="loading" element-loading-text="请稍等,图片上传中">
<!-- 图片上传组件辅助--> <!-- 图片上传组件辅助-->
<el-upload <el-upload
class="avatar-uploader"
name="file"
:action="actionUrl" :action="actionUrl"
:before-upload="beforeUpload"
:data="uploadData"
:headers="headers" :headers="headers"
:on-error="uploadError"
:on-success="uploadSuccess"
:show-file-list="false" :show-file-list="false"
class="avatar-uploader" :data="uploadData"
name="file" :on-success="uploadSuccess"
:on-error="uploadError"
:before-upload="beforeUpload"
/> />
<QuillEditor <Editor
editor-id="wxEditor"
ref="quillEditorRef" ref="quillEditorRef"
v-model="content" :modelValue="content"
:options="editorOptions" @change="(editor) => onEditorChange(editor.getText())"
class="editor"
@change="onEditorChange($event)"
/> />
</div> </div>
</div> </div>
</template> </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>
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
/> />
</el-row> </el-row>
<el-row> <el-row>
<el-icon><Location /></el-icon>
<Icon icon="ep:location" /> <Icon icon="ep:location" />
{{ label }} {{ label }}
</el-row> </el-row>
...@@ -39,6 +38,7 @@ ...@@ -39,6 +38,7 @@
</el-link> </el-link>
</div> </div>
</template> </template>
<script setup lang="ts" name="WxLocation"> <script setup lang="ts" name="WxLocation">
const props = defineProps({ const props = defineProps({
locationX: { locationX: {
......
export enum MsgType {
Event = 'event',
Text = 'text',
Voice = 'voice',
Image = 'image',
Video = 'video',
Link = 'link',
Location = 'location',
Music = 'music',
News = 'news'
}
...@@ -3,8 +3,17 @@ ...@@ -3,8 +3,17 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> <el-form
<WxAccountSelect @change="(accountId) => accountChanged(accountId)" /> class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="公众号" prop="accountId">
<WxMpSelect @change="onAccountChanged" />
</el-form-item>
</el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
...@@ -39,26 +48,32 @@ ...@@ -39,26 +48,32 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script setup name="MpFreePublish"> <script lang="ts" setup name="MpFreePublish">
import * as FreePublishApi from '@/api/mp/freePublish' import * as FreePublishApi from '@/api/mp/freePublish'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据 const list = ref<any[]>([]) // 列表的数据
const queryParams = reactive({
interface QueryParams {
pageNo: number
pageSize: number
accountId?: number
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: undefined
}) })
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (accountId) => { const onAccountChanged = (id: number | undefined) => {
queryParams.accountId = accountId queryParams.accountId = id
getList() getList()
} }
...@@ -75,7 +90,7 @@ const getList = async () => { ...@@ -75,7 +90,7 @@ const getList = async () => {
} }
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (item) => { const handleDelete = async (item: any) => {
try { try {
// 删除的二次确认 // 删除的二次确认
await message.delConfirm('删除后用户将无法访问此页面,确定删除?') await message.delConfirm('删除后用户将无法访问此页面,确定删除?')
......
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
<ContentWrap> <ContentWrap>
<el-form class="-mb-15px" :inline="true" label-width="68px"> <el-form class="-mb-15px" :inline="true" label-width="68px">
<el-form-item label="公众号" prop="accountId"> <el-form-item label="公众号" prop="accountId">
<WxMpSelect @change="accountChange" /> <WxMpSelect @change="onAccountChanged" />
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
<ContentWrap> <ContentWrap>
<el-tabs v-model="type" @tab-change="handleTabChange"> <el-tabs v-model="type" @tab-change="onTabChange">
<!-- tab 1:图片 --> <!-- tab 1:图片 -->
<el-tab-pane name="image"> <el-tab-pane name="image">
<template #label> <template #label>
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
<el-table-column label="文件名" align="center" prop="name" /> <el-table-column label="文件名" align="center" prop="name" />
<el-table-column label="语音" align="center"> <el-table-column label="语音" align="center">
<template #default="scope"> <template #default="scope">
<WxVoicePlayer :url="scope.row.url" /> <WxVoicePlayer v-if="scope.row.url" :url="scope.row.url" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
...@@ -188,10 +188,8 @@ ...@@ -188,10 +188,8 @@
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<!-- <span class="dialog-footer"> -->
<el-button @click="cancelVideo">取 消</el-button> <el-button @click="cancelVideo">取 消</el-button>
<el-button type="primary" @click="submitVideo">提 交</el-button> <el-button type="primary" @click="submitVideo">提 交</el-button>
<!-- </span> -->
</template> </template>
</el-dialog> </el-dialog>
...@@ -203,7 +201,7 @@ ...@@ -203,7 +201,7 @@
<el-table-column label="介绍" align="center" prop="introduction" /> <el-table-column label="介绍" align="center" prop="introduction" />
<el-table-column label="视频" align="center"> <el-table-column label="视频" align="center">
<template #default="scope"> <template #default="scope">
<WxVideoPlayer :url="scope.row.url" /> <WxVideoPlayer v-if="scope.row.url" :url="scope.row.url" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
...@@ -279,14 +277,11 @@ const uploadRules: FormRules = { ...@@ -279,14 +277,11 @@ const uploadRules: FormRules = {
} }
// 素材类型 // 素材类型
type MatertialType = 'image' | 'voice' | 'video' type MaterialType = 'image' | 'voice' | 'video'
const type = ref<MatertialType>('image') const type = ref<MaterialType>('image')
// 遮罩层 const loading = ref(false) // 遮罩层
const loading = ref(false) const list = ref<any[]>([]) // 总条数
// 总条数 const total = ref(0) // 数据列表
const total = ref(0)
// 数据列表
const list = ref([])
// 查询参数 // 查询参数
interface QueryParams { interface QueryParams {
pageNo: number pageNo: number
...@@ -304,7 +299,7 @@ const queryParams: QueryParams = reactive({ ...@@ -304,7 +299,7 @@ const queryParams: QueryParams = reactive({
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
interface UploadData { interface UploadData {
type: MatertialType type: MaterialType
title: string title: string
introduction: string introduction: string
} }
...@@ -319,8 +314,8 @@ const dialogVideoVisible = ref(false) ...@@ -319,8 +314,8 @@ const dialogVideoVisible = ref(false)
const addMaterialLoading = ref(false) const addMaterialLoading = ref(false)
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChange = (accountId: number | undefined) => { const onAccountChanged = (id?: number) => {
queryParams.accountId = accountId queryParams.accountId = id
getList() getList()
} }
...@@ -346,9 +341,9 @@ const handleQuery = () => { ...@@ -346,9 +341,9 @@ const handleQuery = () => {
getList() getList()
} }
const handleTabChange = (tabName: TabPaneName) => { const onTabChange = (tabName: TabPaneName) => {
// 设置 type // 设置 type
uploadData.type = tabName as MatertialType uploadData.type = tabName as MaterialType
// 提前情况数据,避免tab切换后显示垃圾数据 // 提前情况数据,避免tab切换后显示垃圾数据
list.value = [] list.value = []
...@@ -359,54 +354,49 @@ const handleTabChange = (tabName: TabPaneName) => { ...@@ -359,54 +354,49 @@ const handleTabChange = (tabName: TabPaneName) => {
} }
// ======================== 文件上传 ======================== // ======================== 文件上传 ========================
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { const beforeUpload = (rawFile: UploadRawFile, category: 'image' | 'audio' | 'video'): boolean => {
const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes( let allowTypes: string[] = []
rawFile.type let maxSizeMB = 0
) let name = ''
if (!isType) {
message.error('上传图片格式不对!') switch (category) {
return false case 'image':
} allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
const isLt = rawFile.size / 1024 / 1024 < 2 maxSizeMB = 2
if (!isLt) { name = '图片'
message.error('上传图片大小不能超过 2M!') break
return false 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) => { if (!allowTypes.includes(rawFile.type)) {
const isType = ['audio/mp3', 'audio/wma', 'audio/wav', 'audio/amr'].includes(file.type) message.error(`上传${name}格式不对!`)
const isLt = rawFile.size / 1024 / 1024 < 2
if (!isType) {
message.error('上传语音格式不对!')
return false return false
} }
if (!isLt) { // 校验大小
message.error('上传语音大小不能超过 2M!') if (rawFile.size / 1024 / 1024 > maxSizeMB) {
message.error(`上传${name}大小不能超过${maxSizeMB}M!`)
return false return false
} }
loading.value = true loading.value = true
return true return true
} }
const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
const isType = rawFile.type === 'video/mp4' beforeUpload(rawFile, 'image')
if (!isType) {
message.error('上传视频格式不对!')
return false
}
const isLt = rawFile.size / 1024 / 1024 < 10 const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
if (!isLt) { beforeUpload(rawFile, 'audio')
message.error('上传视频大小不能超过 10M!')
return false
}
addMaterialLoading.value = true const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
return true beforeUpload(rawFile, 'video')
}
const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => { const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
loading.value = false loading.value = false
...@@ -441,6 +431,7 @@ const submitVideo = () => { ...@@ -441,6 +431,7 @@ const submitVideo = () => {
}) })
} }
// 弹出 video 新建的表单
const handleAddVideo = () => { const handleAddVideo = () => {
resetVideo() resetVideo()
dialogVideoVisible.value = true dialogVideoVisible.value = true
......
...@@ -2,8 +2,11 @@ ...@@ -2,8 +2,11 @@
<doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" /> <doc-alert title="公众号菜单" url="https://doc.iocoder.cn/mp/menu/" />
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
<WxAccountSelect @change="accountChanged" /> <el-form-item label="公众号" prop="accountId">
<WxMpSelect @change="onAccountChanged" />
</el-form-item>
</el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
...@@ -12,7 +15,7 @@ ...@@ -12,7 +15,7 @@
<!--左边配置菜单--> <!--左边配置菜单-->
<div class="left"> <div class="left">
<div class="weixin-hd"> <div class="weixin-hd">
<div class="weixin-title">{{ name }}</div> <div class="weixin-title">{{ accountName }}</div>
</div> </div>
<div class="weixin-menu menu_main clearfix"> <div class="weixin-menu menu_main clearfix">
<div class="menu_bottom" v-for="(item, i) of menuList" :key="i"> <div class="menu_bottom" v-for="(item, i) of menuList" :key="i">
...@@ -68,7 +71,7 @@ ...@@ -68,7 +71,7 @@
<div v-if="showRightFlag" class="right"> <div v-if="showRightFlag" class="right">
<div class="configure_page"> <div class="configure_page">
<div class="delete_btn"> <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" /> 删除当前菜单<Icon icon="ep:delete" />
</el-button> </el-button>
</div> </div>
...@@ -155,7 +158,7 @@ ...@@ -155,7 +158,7 @@
<div v-else> <div v-else>
<el-row justify="center"> <el-row justify="center">
<el-col :span="24" style="text-align: 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" /> 素材库选择<Icon icon="ep:circle-check" />
</el-button> </el-button>
</el-col> </el-col>
...@@ -185,24 +188,26 @@ ...@@ -185,24 +188,26 @@
</div> </div>
</ContentWrap> </ContentWrap>
</template> </template>
<script setup name="MpMenu"> <script lang="ts" setup name="MpMenu">
import { handleTree } from '@/utils/tree'
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/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 WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
import * as MpMenuApi from '@/api/mp/menu' import * as MpMenuApi from '@/api/mp/menu'
import { handleTree } from '@/utils/tree'
import menuOptions from './menuOptions' import menuOptions from './menuOptions'
const message = useMessage() // 消息 const message = useMessage() // 消息
// ======================== 列表查询 ======================== // ======================== 列表查询 ========================
const loading = ref(true) // 遮罩层 const loading = ref(false) // 遮罩层
const accountId = ref(undefined) // 公众号Id const accountId = ref<number | undefined>()
const menuList = ref({ children: [] }) const accountName = ref<string | undefined>('')
const menuList = ref<any>({ children: [] })
// ======================== 菜单操作 ======================== // ======================== 菜单操作 ========================
const isActive = ref(-1) // 一级菜单点中样式 const isActive = ref(-1) // 一级菜单点中样式
const isSubMenuActive = ref(-1) // 一级菜单点中样式 const isSubMenuActive = ref<string | number>(-1) // 一级菜单点中样式
const isSubMenuFlag = ref(-1) // 二级菜单显示标志 const isSubMenuFlag = ref(-1) // 二级菜单显示标志
// ======================== 菜单编辑 ======================== // ======================== 菜单编辑 ========================
...@@ -210,15 +215,16 @@ const showRightFlag = ref(false) // 右边配置显示默认详情还是配置 ...@@ -210,15 +215,16 @@ const showRightFlag = ref(false) // 右边配置显示默认详情还是配置
const nameMaxLength = ref(0) // 菜单名称最大长度;1 级是 4 字符;2 级是 7 字符; const nameMaxLength = ref(0) // 菜单名称最大长度;1 级是 4 字符;2 级是 7 字符;
const showConfigureContent = ref(true) // 是否展示配置内容;如果有子菜单,就不显示配置内容 const showConfigureContent = ref(true) // 是否展示配置内容;如果有子菜单,就不显示配置内容
const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件 const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件
const tempObj = ref({}) // 右边临时变量,作为中间值牵引关系 const tempObj = ref<any>({}) // 右边临时变量,作为中间值牵引关系
// 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数 // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数
const tempSelfObj = ref({}) const tempSelfObj = ref<any>({})
const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗 const dialogNewsVisible = ref(false) // 跳转图文时的素材选择弹窗
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (id) => { const onAccountChanged = (id?: number, name?: string) => {
accountId.value = id accountId.value = id
accountName.value = name
getList() getList()
} }
...@@ -241,10 +247,10 @@ const handleQuery = () => { ...@@ -241,10 +247,10 @@ const handleQuery = () => {
} }
// 将后端返回的 menuList,转换成前端的 menuList // 将后端返回的 menuList,转换成前端的 menuList
const convertMenuList = (list) => { const convertMenuList = (list: any[]) => {
if (!list) return [] if (!list) return []
const menuList = [] const result: any[] = []
list.forEach((item) => { list.forEach((item) => {
const menu = { const menu = {
...item ...item
...@@ -271,9 +277,9 @@ const convertMenuList = (list) => { ...@@ -271,9 +277,9 @@ const convertMenuList = (list) => {
hqMusicUrl: item.replyHqMusicUrl hqMusicUrl: item.replyHqMusicUrl
} }
} }
menuList.push(menu) result.push(menu)
}) })
return menuList return result
} }
// 重置表单,清空表单数据 // 重置表单,清空表单数据
...@@ -286,7 +292,7 @@ const resetForm = () => { ...@@ -286,7 +292,7 @@ const resetForm = () => {
// 菜单编辑 // 菜单编辑
showRightFlag.value = false showRightFlag.value = false
nameMaxLength.value = 0 nameMaxLength.value = 0
showConfigureContent.value = 0 showConfigureContent.value = false
hackResetWxReplySelect.value = false hackResetWxReplySelect.value = false
tempObj.value = {} tempObj.value = {}
tempSelfObj.value = {} tempSelfObj.value = {}
...@@ -295,7 +301,7 @@ const resetForm = () => { ...@@ -295,7 +301,7 @@ const resetForm = () => {
// ======================== 菜单操作 ======================== // ======================== 菜单操作 ========================
// 一级菜单点击事件 // 一级菜单点击事件
const menuClick = (i, item) => { const menuClick = (i: number, item: any) => {
// 右侧的表单相关 // 右侧的表单相关
resetEditor() resetEditor()
showRightFlag.value = true // 右边菜单 showRightFlag.value = true // 右边菜单
...@@ -312,11 +318,10 @@ const menuClick = (i, item) => { ...@@ -312,11 +318,10 @@ const menuClick = (i, item) => {
} }
// 二级菜单点击事件 // 二级菜单点击事件
const subMenuClick = (subItem, index, k) => { const subMenuClick = (subItem: any, index: number, k: number) => {
// 右侧的表单相关 // 右侧的表单相关
resetEditor() resetEditor()
showRightFlag.value = true // 右边菜单 showRightFlag.value = true // 右边菜单
console.log(subItem)
tempObj.value = subItem // 将点击的数据放到临时变量,对象有引用作用 tempObj.value = subItem // 将点击的数据放到临时变量,对象有引用作用
tempSelfObj.value.grand = '2' // 表示二级菜单 tempSelfObj.value.grand = '2' // 表示二级菜单
tempSelfObj.value.index = index // 表示一级菜单索引 tempSelfObj.value.index = index // 表示一级菜单索引
...@@ -331,7 +336,7 @@ const subMenuClick = (subItem, index, k) => { ...@@ -331,7 +336,7 @@ const subMenuClick = (subItem, index, k) => {
// 添加横向一级菜单 // 添加横向一级菜单
const addMenu = () => { const addMenu = () => {
const menuKeyLength = menuList.value.length const menuKeyLength: number = menuList.value.length
const addButton = { const addButton = {
name: '菜单名称', name: '菜单名称',
children: [], children: [],
...@@ -342,10 +347,10 @@ const addMenu = () => { ...@@ -342,10 +347,10 @@ const addMenu = () => {
} }
} }
menuList.value[menuKeyLength] = addButton menuList.value[menuKeyLength] = addButton
menuClick(menuKeyLength.value - 1, addButton) menuClick(menuKeyLength - 1, addButton)
} }
// 添加横向二级菜单;item 表示要操作的父菜单 // 添加横向二级菜单;item 表示要操作的父菜单
const addSubMenu = (i, item) => { const addSubMenu = (i: number, item: any) => {
// 清空父菜单的属性,因为它只需要 name 属性即可 // 清空父菜单的属性,因为它只需要 name 属性即可
if (!item.children || item.children.length <= 0) { if (!item.children || item.children.length <= 0) {
item.children = [] item.children = []
...@@ -361,8 +366,8 @@ const addSubMenu = (i, item) => { ...@@ -361,8 +366,8 @@ const addSubMenu = (i, item) => {
showConfigureContent.value = false showConfigureContent.value = false
} }
let subMenuKeyLength = item.children.length // 获取二级菜单key长度 const subMenuKeyLength = item.children.length // 获取二级菜单key长度
let addButton = { const addButton = {
name: '子菜单名称', name: '子菜单名称',
reply: { reply: {
// 用于存储回复内容 // 用于存储回复内容
...@@ -399,7 +404,7 @@ const handleDeleteMenu = async () => { ...@@ -399,7 +404,7 @@ const handleDeleteMenu = async () => {
// ======================== 菜单编辑 ======================== // ======================== 菜单编辑 ========================
const handleSave = async () => { const handleSave = async () => {
try { try {
await message.confirm('确定要删除吗?') await message.confirm('确定要保存吗?')
loading.value = true loading.value = true
await MpMenuApi.saveMenu(accountId.value, convertMenuFormList()) await MpMenuApi.saveMenu(accountId.value, convertMenuFormList())
getList() getList()
...@@ -413,7 +418,6 @@ const handleSave = async () => { ...@@ -413,7 +418,6 @@ const handleSave = async () => {
const resetEditor = () => { const resetEditor = () => {
hackResetWxReplySelect.value = false // 销毁组件 hackResetWxReplySelect.value = false // 销毁组件
nextTick(() => { nextTick(() => {
console.log('nextTick')
hackResetWxReplySelect.value = true // 重建组件 hackResetWxReplySelect.value = true // 重建组件
}) })
} }
...@@ -432,9 +436,9 @@ const handleDelete = async () => { ...@@ -432,9 +436,9 @@ const handleDelete = async () => {
// 将前端的 menuList,转换成后端接收的 menuList // 将前端的 menuList,转换成后端接收的 menuList
const convertMenuFormList = () => { const convertMenuFormList = () => {
const result = [] const result: any[] = []
menuList.value.forEach((item) => { menuList.value.forEach((item) => {
let menu = convertMenuForm(item) const menu = convertMenuForm(item)
result.push(menu) result.push(menu)
// 处理子菜单 // 处理子菜单
...@@ -450,7 +454,7 @@ const convertMenuFormList = () => { ...@@ -450,7 +454,7 @@ const convertMenuFormList = () => {
} }
// 将前端的 menu,转换成后端接收的 menu // 将前端的 menu,转换成后端接收的 menu
const convertMenuForm = (menu) => { const convertMenuForm = (menu: any) => {
let result = { let result = {
...menu, ...menu,
children: undefined, // 不处理子节点 children: undefined, // 不处理子节点
...@@ -473,11 +477,7 @@ const convertMenuForm = (menu) => { ...@@ -473,11 +477,7 @@ const convertMenuForm = (menu) => {
} }
// ======================== 菜单编辑(素材选择) ======================== // ======================== 菜单编辑(素材选择) ========================
const openMaterial = () => { const selectMaterial = (item: any) => {
dialogNewsVisible.value = true
}
const selectMaterial = (item) => {
const articleId = item.articleId const articleId = item.articleId
const articles = item.content.newsItem const articles = item.content.newsItem
// 提示,针对多图文 // 提示,针对多图文
......
...@@ -9,14 +9,7 @@ ...@@ -9,14 +9,7 @@
label-width="68px" label-width="68px"
> >
<el-form-item label="公众号" prop="accountId"> <el-form-item label="公众号" prop="accountId">
<el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> <WxMpSelect @change="onAccountChanged" />
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="消息类型" prop="type"> <el-form-item label="消息类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px"> <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px">
...@@ -84,70 +77,76 @@ ...@@ -84,70 +77,76 @@
<el-table-column label="内容" prop="content"> <el-table-column label="内容" prop="content">
<template #default="scope"> <template #default="scope">
<!-- 【事件】区域 --> <!-- 【事件】区域 -->
<div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'"> <div v-if="scope.row.type === MsgType.Event && scope.row.event === 'subscribe'">
<el-tag type="success">关注</el-tag> <el-tag type="success">关注</el-tag>
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'"> <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'unsubscribe'">
<el-tag type="danger">取消关注</el-tag> <el-tag type="danger">取消关注</el-tag>
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'"> <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'CLICK'">
<el-tag>点击菜单</el-tag> <el-tag>点击菜单</el-tag>
{{ scope.row.eventKey }} {{ scope.row.eventKey }}
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'"> <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'VIEW'">
<el-tag>点击菜单链接</el-tag> <el-tag>点击菜单链接</el-tag>
{{ scope.row.eventKey }} {{ scope.row.eventKey }}
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'"> <div
v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_waitmsg'"
>
<el-tag>扫码结果</el-tag> <el-tag>扫码结果</el-tag>
{{ scope.row.eventKey }} {{ scope.row.eventKey }}
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'"> <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_push'">
<el-tag>扫码结果</el-tag> <el-tag>扫码结果</el-tag>
{{ scope.row.eventKey }} {{ scope.row.eventKey }}
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'"> <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_sysphoto'">
<el-tag>系统拍照发图</el-tag> <el-tag>系统拍照发图</el-tag>
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'"> <div
v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_photo_or_album'"
>
<el-tag>拍照或者相册</el-tag> <el-tag>拍照或者相册</el-tag>
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'"> <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_weixin'">
<el-tag>微信相册</el-tag> <el-tag>微信相册</el-tag>
</div> </div>
<div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'"> <div
v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'location_select'"
>
<el-tag>选择地理位置</el-tag> <el-tag>选择地理位置</el-tag>
</div> </div>
<div v-else-if="scope.row.type === 'event'"> <div v-else-if="scope.row.type === MsgType.Event">
<el-tag type="danger">未知事件类型</el-tag> <el-tag type="danger">未知事件类型</el-tag>
</div> </div>
<!-- 【消息】区域 --> <!-- 【消息】区域 -->
<div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div> <div v-else-if="scope.row.type === MsgType.Text">{{ scope.row.content }}</div>
<div v-else-if="scope.row.type === 'voice'"> <div v-else-if="scope.row.type === MsgType.Voice">
<wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" /> <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" />
</div> </div>
<div v-else-if="scope.row.type === 'image'"> <div v-else-if="scope.row.type === MsgType.Image">
<a target="_blank" :href="scope.row.mediaUrl"> <a target="_blank" :href="scope.row.mediaUrl">
<img :src="scope.row.mediaUrl" style="width: 100px" /> <img :src="scope.row.mediaUrl" style="width: 100px" />
</a> </a>
</div> </div>
<div v-else-if="scope.row.type === 'video' || scope.row.type === 'shortvideo'"> <div v-else-if="scope.row.type === MsgType.Video || scope.row.type === 'shortvideo'">
<wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" /> <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
</div> </div>
<div v-else-if="scope.row.type === 'link'"> <div v-else-if="scope.row.type === MsgType.Link">
<el-tag>链接</el-tag> <el-tag>链接</el-tag>
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a> <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
</div> </div>
<div v-else-if="scope.row.type === 'location'"> <div v-else-if="scope.row.type === MsgType.Location">
<wx-location <WxLocation
:label="scope.row.label" :label="scope.row.label"
:location-y="scope.row.locationY" :location-y="scope.row.locationY"
:location-x="scope.row.locationX" :location-x="scope.row.locationX"
/> />
</div> </div>
<div v-else-if="scope.row.type === 'music'"> <div v-else-if="scope.row.type === MsgType.Music">
<wx-music <WxMusic
:title="scope.row.title" :title="scope.row.title"
:description="scope.row.description" :description="scope.row.description"
:thumb-media-url="scope.row.thumbMediaUrl" :thumb-media-url="scope.row.thumbMediaUrl"
...@@ -155,8 +154,8 @@ ...@@ -155,8 +154,8 @@
:hq-music-url="scope.row.hqMusicUrl" :hq-music-url="scope.row.hqMusicUrl"
/> />
</div> </div>
<div v-else-if="scope.row.type === 'news'"> <div v-else-if="scope.row.type === MsgType.News">
<wx-news :articles="scope.row.articles" /> <WxNews :articles="scope.row.articles" />
</div> </div>
<div v-else> <div v-else>
<el-tag type="danger">未知消息类型</el-tag> <el-tag type="danger">未知消息类型</el-tag>
...@@ -177,7 +176,7 @@ ...@@ -177,7 +176,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页组件 --> <!-- 分页组件 -->
<pagination <Pagination
v-show="total > 0" v-show="total > 0"
:total="total" :total="total"
v-model:page="queryParams.pageNo" v-model:page="queryParams.pageNo"
...@@ -186,9 +185,14 @@ ...@@ -186,9 +185,14 @@
/> />
<!-- 发送消息的弹窗 --> <!-- 发送消息的弹窗 -->
<el-dialog title="粉丝消息列表" v-model="open" @click="openDialog()" width="50%"> <el-dialog
title="粉丝消息列表"
v-model="showMessageBox"
@click="showMessageBox = true"
width="50%"
>
<template #footer> <template #footer>
<wx-msg :user-id="userId" v-if="open" /> <WxMsg :user-id="userId" v-if="showMessageBox" />
</template> </template>
</el-dialog> </el-dialog>
</ContentWrap> </ContentWrap>
...@@ -200,17 +204,27 @@ import WxMsg from '@/views/mp/components/wx-msg/main.vue' ...@@ -200,17 +204,27 @@ import WxMsg from '@/views/mp/components/wx-msg/main.vue'
import WxLocation from '@/views/mp/components/wx-location/main.vue' import WxLocation from '@/views/mp/components/wx-location/main.vue'
import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxMusic from '@/views/mp/components/wx-music/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news/main.vue'
import * as MpAccountApi from '@/api/mp/account' import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
import * as MpMessageApi from '@/api/mp/message' import * as MpMessageApi from '@/api/mp/message'
const message = useMessage() // 消息弹窗
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' 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 loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据 const list = ref<any[]>([]) // 列表的数据
const queryParams = reactive({
// 搜索参数
interface QueryParams {
pageNo: number
pageSize: number
openid: string | null
accountId: number | null
type: MsgType | null
createTime: string[] | []
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
openid: null, openid: null,
...@@ -218,19 +232,18 @@ const queryParams = reactive({ ...@@ -218,19 +232,18 @@ const queryParams = reactive({
type: null, type: null,
createTime: [] createTime: []
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
// TODO 芋艿:下面应该移除 const showMessageBox = ref(false) // 是否显示弹出层
const open = ref(false) // 是否显示弹出层
const userId = ref(0) // 操作的用户编号 const userId = ref(0) // 操作的用户编号
const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
/** 侦听accountId */
const onAccountChanged = (id?: number) => {
queryParams.accountId = id as number
handleQuery()
}
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
// 如果没有选中公众号账号,则进行提示。
if (!queryParams.accountId) {
await message.error('未选中公众号,无法查询消息')
return
}
try { try {
loading.value = true loading.value = true
const data = await MpMessageApi.getMessagePage(queryParams) const data = await MpMessageApi.getMessagePage(queryParams)
...@@ -249,34 +262,15 @@ const handleQuery = () => { ...@@ -249,34 +262,15 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = async () => { const resetQuery = async () => {
queryFormRef.value.resetFields() const accountId = queryParams.accountId
// 默认选中第一个 queryFormRef.value?.resetFields()
if (accountList.value.length > 0) { queryParams.accountId = accountId
// @ts-ignore
queryParams.accountId = accountList.value[0].id
}
handleQuery() handleQuery()
} }
const handleSend = async (row) => {
userId.value = row.userId
open.value = true
}
const openDialog = () => { /** 打开消息发送窗口 */
open.value = true const handleSend = async (row: any) => {
userId.value = row.userId
showMessageBox.value = true
} }
// const closeDiaLog = () => {
// open.value = false
// }
/** 初始化 **/
onMounted(async () => {
accountList.value = await MpAccountApi.getSimpleAccountList()
// 选中第一个
if (accountList.value.length > 0) {
// @ts-ignore
queryParams.accountId = accountList.value[0].id
}
await getList()
})
</script> </script>
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
</template> </template>
<script lang="ts" name="MpTagForm" setup> <script lang="ts" name="MpTagForm" setup>
import * as MpTagApi from '@/api/mp/tag' import * as MpTagApi from '@/api/mp/tag'
import type { FormInstance, FormRules } from 'element-plus'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
...@@ -26,18 +27,22 @@ const message = useMessage() // 消息弹窗 ...@@ -26,18 +27,22 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示 const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题 const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改 const formType = ref<'create' | 'update' | ''>('') // 表单的类型:create - 新增;update - 修改
const formData = ref({ const formData = ref({
accountId: -1, accountId: -1,
name: '' name: ''
}) })
const formRules = reactive({ const formRules: FormRules = {
name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }] 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 dialogVisible.value = true
dialogTitle.value = t('action.' + type) dialogTitle.value = t('action.' + type)
formType.value = type formType.value = type
...@@ -56,11 +61,10 @@ const open = async (type: string, accountId: number, id?: number) => { ...@@ -56,11 +61,10 @@ const open = async (type: string, accountId: number, id?: number) => {
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */ /** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => { const submitForm = async () => {
// 校验表单 // 校验表单
if (!formRef) return if (!formRef) return
const valid = await formRef.value.validate() const valid = await formRef.value?.validate()
if (!valid) return if (!valid) return
// 提交请求 // 提交请求
formLoading.value = true formLoading.value = true
......
...@@ -3,17 +3,25 @@ ...@@ -3,17 +3,25 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> <el-form
<WxAccountSelect @change="accountChanged"> class="-mb-15px"
<template #actions> :model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="公众号" prop="accountId">
<WxMpSelect @change="onAccountChanged" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']"> <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> </el-button>
<el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']"> <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']">
<Icon icon="ep:refresh" class="mr-5px" /> 同步 <Icon icon="ep:refresh" class="mr-5px" /> 同步
</el-button> </el-button>
</template> </el-form-item>
</WxAccountSelect> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
...@@ -64,25 +72,32 @@ ...@@ -64,25 +72,32 @@
</template> </template>
<script setup lang="ts" name="MpTag"> <script setup lang="ts" name="MpTag">
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
import * as MpTagApi from '@/api/mp/tag' import * as MpTagApi from '@/api/mp/tag'
import TagForm from './TagForm.vue' import TagForm from './TagForm.vue'
import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据 const list = ref<any>([]) // 列表的数据
const queryParams = reactive({
interface QueryParams {
pageNo: number
pageSize: number
accountId?: number
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: undefined
}) })
const formRef = ref<InstanceType<typeof TagForm> | null>(null)
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (accountId) => { const onAccountChanged = (id?: number) => {
queryParams.pageNo = 1 queryParams.pageNo = 1
queryParams.accountId = accountId queryParams.accountId = id
getList() getList()
} }
...@@ -99,9 +114,8 @@ const getList = async () => { ...@@ -99,9 +114,8 @@ const getList = async () => {
} }
/** 添加/修改操作 */ /** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => { 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) => { ...@@ -121,8 +135,7 @@ const handleDelete = async (id: number) => {
const handleSync = async () => { const handleSync = async () => {
try { try {
await message.confirm('是否确认同步标签?') await message.confirm('是否确认同步标签?')
// @ts-ignore await MpTagApi.syncTag(queryParams.accountId as number)
await MpTagApi.syncTag(queryParams.accountId)
message.success('同步标签成功') message.success('同步标签成功')
await getList() await getList()
} catch {} } catch {}
......
...@@ -3,14 +3,42 @@ ...@@ -3,14 +3,42 @@
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect --> <el-form
<WxAccountSelect @change="(accountId) => accountChanged(accountId)"> class="-mb-15px"
<template #actions> :model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="公众号" prop="accountId">
<WxMpSelect @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']"> <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']">
<Icon icon="ep:refresh" class="mr-5px" /> 同步 <Icon icon="ep:refresh" class="mr-5px" /> 同步
</el-button> </el-button>
</template> </el-form-item>
</WxAccountSelect> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
...@@ -66,35 +94,40 @@ ...@@ -66,35 +94,40 @@
<UserForm ref="formRef" @success="getList" /> <UserForm ref="formRef" @success="getList" />
</template> </template>
<script lang="ts" setup name="MpUser"> <script lang="ts" setup name="MpUser">
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import * as MpUserApi from '@/api/mp/user' import * as MpUserApi from '@/api/mp/user'
import * as MpTagApi from '@/api/mp/tag' import * as MpTagApi from '@/api/mp/tag'
import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
import type { FormInstance } from 'element-plus'
import UserForm from './UserForm.vue' import UserForm from './UserForm.vue'
const message = useMessage() // 消息 const message = useMessage() // 消息
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据 const list = ref<any[]>([]) // 列表的数据
const queryParams = reactive({
interface QueryParams {
pageNo: number
pageSize: number
accountId?: number
openid: string | null
nickname: string | null
}
const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: null, accountId: undefined,
openid: null, openid: null,
nickname: null nickname: null
}) })
const tagList = ref([]) // 公众号标签列表 const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
const tagList = ref<any[]>([]) // 公众号标签列表
/** 初始化 */
onMounted(async () => {
tagList.value = await MpTagApi.getSimpleTagList()
})
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const accountChanged = (accountId) => { const onAccountChanged = (id?: number) => {
queryParams.pageNo = 1 queryParams.pageNo = 1
queryParams.accountId = accountId queryParams.accountId = id
getList() getList()
} }
...@@ -110,20 +143,38 @@ const getList = async () => { ...@@ -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) => { const openForm = (id: number) => {
formRef.value.open(id) formRef.value?.open(id)
} }
/** 同步标签 */ /** 同步标签 */
const handleSync = async () => { const handleSync = async () => {
const accountId = queryParams.accountId
try { try {
await message.confirm('是否确认同步粉丝?') await message.confirm('是否确认同步粉丝?')
await MpUserApi.syncUser(accountId) await MpUserApi.syncUser(queryParams.accountId)
message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询') message.success('开始从微信公众号同步粉丝信息,同步需要一段时间,建议稍后再查询')
await getList() await getList()
} catch {} } catch {}
} }
/** 初始化 */
onMounted(async () => {
tagList.value = await MpTagApi.getSimpleTagList()
})
</script> </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