mirror of
				https://gitee.com/hhyykk/ipms-sjy-ui.git
				synced 2025-11-04 12:18:43 +08:00 
			
		
		
		
	Merge remote-tracking branch 'yudao/dev' into dev
# Conflicts: # src/views/mp/components/wx-editor/WxEditor.vue # src/views/mp/tag/TagForm.vue
This commit is contained in:
		@@ -3,13 +3,16 @@
 | 
			
		||||
 | 
			
		||||
  <!-- 搜索工作栏 -->
 | 
			
		||||
  <ContentWrap>
 | 
			
		||||
    <!-- TODO @芋艿:调整成 el-form 和 WxAccountSelect  -->
 | 
			
		||||
    <WxAccountSelect @change="accountChanged" />
 | 
			
		||||
    <el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px">
 | 
			
		||||
      <el-form-item label="公众号" prop="accountId">
 | 
			
		||||
        <WxMpSelect @change="onAccountChanged" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <!-- tab 切换 -->
 | 
			
		||||
  <ContentWrap>
 | 
			
		||||
    <el-tabs v-model="type" @tab-change="handleTabChange">
 | 
			
		||||
    <el-tabs v-model="msgType" @tab-change="handleTabChange">
 | 
			
		||||
      <!-- 操作工具栏 -->
 | 
			
		||||
      <el-row :gutter="10" class="mb8">
 | 
			
		||||
        <el-col :span="1.5">
 | 
			
		||||
@@ -18,26 +21,26 @@
 | 
			
		||||
            plain
 | 
			
		||||
            @click="handleAdd"
 | 
			
		||||
            v-hasPermi="['mp:auto-reply:create']"
 | 
			
		||||
            v-if="type !== '1' || list.length <= 0"
 | 
			
		||||
            v-if="msgType !== MsgType.Follow || list.length <= 0"
 | 
			
		||||
          >
 | 
			
		||||
            <Icon icon="ep:plus" />新增
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </el-col>
 | 
			
		||||
      </el-row>
 | 
			
		||||
      <!-- tab 项 -->
 | 
			
		||||
      <el-tab-pane name="1">
 | 
			
		||||
      <el-tab-pane :name="MsgType.Follow">
 | 
			
		||||
        <template #label>
 | 
			
		||||
          <span><Icon icon="ep:star-off" /> 关注时回复</span>
 | 
			
		||||
          <span><Icon icon="ep:star" /> 关注时回复</span>
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-tab-pane>
 | 
			
		||||
      <el-tab-pane name="2">
 | 
			
		||||
      <el-tab-pane :name="MsgType.Message">
 | 
			
		||||
        <template #label>
 | 
			
		||||
          <span><Icon icon="ep:chat-line-round" /> 消息回复</span>
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-tab-pane>
 | 
			
		||||
      <el-tab-pane name="3">
 | 
			
		||||
      <el-tab-pane :name="MsgType.Keyword">
 | 
			
		||||
        <template #label>
 | 
			
		||||
          <span><Icon icon="ep:news" /> 关键词回复</span>
 | 
			
		||||
          <span><Icon icon="fa:newspaper-o" /> 关键词回复</span>
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-tab-pane>
 | 
			
		||||
    </el-tabs>
 | 
			
		||||
@@ -47,10 +50,20 @@
 | 
			
		||||
        label="请求消息类型"
 | 
			
		||||
        align="center"
 | 
			
		||||
        prop="requestMessageType"
 | 
			
		||||
        v-if="type === '2'"
 | 
			
		||||
        v-if="msgType === MsgType.Message"
 | 
			
		||||
      />
 | 
			
		||||
      <el-table-column label="关键词" align="center" prop="requestKeyword" v-if="type === '3'" />
 | 
			
		||||
      <el-table-column label="匹配类型" align="center" prop="requestMatch" v-if="type === '3'">
 | 
			
		||||
      <el-table-column
 | 
			
		||||
        label="关键词"
 | 
			
		||||
        align="center"
 | 
			
		||||
        prop="requestKeyword"
 | 
			
		||||
        v-if="msgType === MsgType.Keyword"
 | 
			
		||||
      />
 | 
			
		||||
      <el-table-column
 | 
			
		||||
        label="匹配类型"
 | 
			
		||||
        align="center"
 | 
			
		||||
        prop="requestMatch"
 | 
			
		||||
        v-if="msgType === MsgType.Keyword"
 | 
			
		||||
      >
 | 
			
		||||
        <template #default="scope">
 | 
			
		||||
          <dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
 | 
			
		||||
        </template>
 | 
			
		||||
@@ -64,7 +77,7 @@
 | 
			
		||||
        <template #default="scope">
 | 
			
		||||
          <div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
 | 
			
		||||
          <div v-else-if="scope.row.responseMessageType === 'voice'">
 | 
			
		||||
            <WxVoicePlayer :url="scope.row.responseMediaUrl" />
 | 
			
		||||
            <WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.responseMessageType === 'image'">
 | 
			
		||||
            <a target="_blank" :href="scope.row.responseMediaUrl">
 | 
			
		||||
@@ -77,7 +90,11 @@
 | 
			
		||||
              scope.row.responseMessageType === 'shortvideo'
 | 
			
		||||
            "
 | 
			
		||||
          >
 | 
			
		||||
            <WxVideoPlayer :url="scope.row.responseMediaUrl" style="margin-top: 10px" />
 | 
			
		||||
            <WxVideoPlayer
 | 
			
		||||
              v-if="scope.row.responseMediaUrl"
 | 
			
		||||
              :url="scope.row.responseMediaUrl"
 | 
			
		||||
              style="margin-top: 10px"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.responseMessageType === 'news'">
 | 
			
		||||
            <WxNews :articles="scope.row.responseArticles" />
 | 
			
		||||
@@ -123,21 +140,21 @@
 | 
			
		||||
    </el-table>
 | 
			
		||||
 | 
			
		||||
    <!-- 添加或修改自动回复的对话框 -->
 | 
			
		||||
    <el-dialog :title="title" v-model="open" width="800px" append-to-body>
 | 
			
		||||
      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
 | 
			
		||||
        <el-form-item label="消息类型" prop="requestMessageType" v-if="type === '2'">
 | 
			
		||||
          <el-select v-model="form.requestMessageType" placeholder="请选择">
 | 
			
		||||
    <el-dialog :title="title" v-model="showReplyFormDialog" width="800px" append-to-body>
 | 
			
		||||
      <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
 | 
			
		||||
        <el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message">
 | 
			
		||||
          <el-select v-model="replyForm.requestMessageType" placeholder="请选择">
 | 
			
		||||
            <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value">
 | 
			
		||||
              <el-option
 | 
			
		||||
                v-if="requestMessageTypes.includes(dict.value)"
 | 
			
		||||
                v-if="RequestMessageTypes.includes(dict.value)"
 | 
			
		||||
                :label="dict.label"
 | 
			
		||||
                :value="dict.value"
 | 
			
		||||
              />
 | 
			
		||||
            </template>
 | 
			
		||||
          </el-select>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="匹配类型" prop="requestMatch" v-if="type === '3'">
 | 
			
		||||
          <el-select v-model="form.requestMatch" placeholder="请选择匹配类型" clearable>
 | 
			
		||||
        <el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
 | 
			
		||||
          <el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
 | 
			
		||||
            <el-option
 | 
			
		||||
              v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
 | 
			
		||||
              :key="dict.value"
 | 
			
		||||
@@ -146,8 +163,8 @@
 | 
			
		||||
            />
 | 
			
		||||
          </el-select>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="关键词" prop="requestKeyword" v-if="type === '3'">
 | 
			
		||||
          <el-input v-model="form.requestKeyword" placeholder="请输入内容" clearable />
 | 
			
		||||
        <el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
 | 
			
		||||
          <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="回复消息">
 | 
			
		||||
          <WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" />
 | 
			
		||||
@@ -160,38 +177,47 @@
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup name="MpAutoReply">
 | 
			
		||||
<script setup lang="ts" name="MpAutoReply">
 | 
			
		||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
 | 
			
		||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
 | 
			
		||||
import WxMusic from '@/views/mp/components/wx-music/main.vue'
 | 
			
		||||
import WxNews from '@/views/mp/components/wx-news/main.vue'
 | 
			
		||||
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
 | 
			
		||||
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 | 
			
		||||
import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 | 
			
		||||
import * as MpAutoReplyApi from '@/api/mp/autoReply'
 | 
			
		||||
 | 
			
		||||
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
 | 
			
		||||
import { dateFormatter } from '@/utils/formatTime'
 | 
			
		||||
import { ContentWrap } from '@/components/ContentWrap'
 | 
			
		||||
import type { TabPaneName } from 'element-plus'
 | 
			
		||||
 | 
			
		||||
const message = useMessage()
 | 
			
		||||
 | 
			
		||||
// const queryFormRef = ref()
 | 
			
		||||
const formRef = ref()
 | 
			
		||||
 | 
			
		||||
// tab 类型(1、关注时回复;2、消息回复;3、关键词回复)
 | 
			
		||||
const type = ref('3')
 | 
			
		||||
// 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复)
 | 
			
		||||
// 作为tab.name
 | 
			
		||||
enum MsgType {
 | 
			
		||||
  Follow = 1,
 | 
			
		||||
  Message = 2,
 | 
			
		||||
  Keyword = 3
 | 
			
		||||
}
 | 
			
		||||
const msgType = ref<MsgType>(MsgType.Keyword)
 | 
			
		||||
// 允许选择的请求消息类型
 | 
			
		||||
const requestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link']
 | 
			
		||||
const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link']
 | 
			
		||||
// 遮罩层
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
// 显示搜索条件
 | 
			
		||||
// const showSearch = ref(true)
 | 
			
		||||
// 总条数
 | 
			
		||||
const total = ref(0)
 | 
			
		||||
// 自动回复列表
 | 
			
		||||
const list = ref([])
 | 
			
		||||
const list = ref<any[]>([])
 | 
			
		||||
 | 
			
		||||
// 查询参数
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
interface QueryParams {
 | 
			
		||||
  pageNo: number
 | 
			
		||||
  pageSize: number
 | 
			
		||||
  accountId?: number
 | 
			
		||||
}
 | 
			
		||||
const queryParams: QueryParams = reactive({
 | 
			
		||||
  pageNo: 1,
 | 
			
		||||
  pageSize: 10,
 | 
			
		||||
  accountId: undefined
 | 
			
		||||
@@ -200,12 +226,50 @@ const queryParams = reactive({
 | 
			
		||||
// 弹出层标题
 | 
			
		||||
const title = ref('')
 | 
			
		||||
// 是否显示弹出层
 | 
			
		||||
const open = ref(false)
 | 
			
		||||
const showReplyFormDialog = ref(false)
 | 
			
		||||
// 表单参数
 | 
			
		||||
const form = ref({})
 | 
			
		||||
type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link'
 | 
			
		||||
interface ReplyForm {
 | 
			
		||||
  // relation:
 | 
			
		||||
  id?: number
 | 
			
		||||
  accountId?: number
 | 
			
		||||
  type?: MsgType
 | 
			
		||||
  // request:
 | 
			
		||||
  requestMessageType?: ReplyType
 | 
			
		||||
  requestMatch?: number
 | 
			
		||||
  requestKeyword?: string
 | 
			
		||||
  // response:
 | 
			
		||||
  responseMessageType?: ReplyType
 | 
			
		||||
  responseContent?: string
 | 
			
		||||
  responseMediaId?: number
 | 
			
		||||
  responseMediaUrl?: string
 | 
			
		||||
  responseTitle?: string
 | 
			
		||||
  responseDescription?: number
 | 
			
		||||
  responseThumbMediaId?: string
 | 
			
		||||
  responseThumbMediaUrl?: string
 | 
			
		||||
  responseArticles?: any[]
 | 
			
		||||
  responseMusicUrl?: string
 | 
			
		||||
  responseHqMusicUrl?: string
 | 
			
		||||
}
 | 
			
		||||
interface ObjData {
 | 
			
		||||
  type: ReplyType
 | 
			
		||||
  accountId?: number
 | 
			
		||||
  content?: string
 | 
			
		||||
  mediaId?: number
 | 
			
		||||
  url?: string
 | 
			
		||||
  title?: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  thumbMediaId?: number
 | 
			
		||||
  thumbMediaUrl?: string
 | 
			
		||||
  articles?: any[]
 | 
			
		||||
  musicUrl?: string
 | 
			
		||||
  hqMusicUrl?: string
 | 
			
		||||
}
 | 
			
		||||
const replyForm = ref<ReplyForm>({})
 | 
			
		||||
// 回复消息
 | 
			
		||||
const objData = ref({
 | 
			
		||||
  type: 'text'
 | 
			
		||||
const objData = ref<ObjData>({
 | 
			
		||||
  type: 'text',
 | 
			
		||||
  accountId: undefined
 | 
			
		||||
})
 | 
			
		||||
// 表单校验
 | 
			
		||||
const rules = {
 | 
			
		||||
@@ -216,8 +280,8 @@ const rules = {
 | 
			
		||||
// 重置 WxReplySelect 组件,解决无法清除的问题
 | 
			
		||||
const hackResetWxReplySelect = ref(false)
 | 
			
		||||
 | 
			
		||||
const accountChanged = (accountId) => {
 | 
			
		||||
  queryParams.accountId = accountId
 | 
			
		||||
const onAccountChanged = (id?: number) => {
 | 
			
		||||
  queryParams.accountId = id
 | 
			
		||||
  getList()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -227,7 +291,7 @@ const getList = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    const data = await MpAutoReplyApi.getAutoReplyPage({
 | 
			
		||||
      ...queryParams,
 | 
			
		||||
      type: type.value
 | 
			
		||||
      type: msgType.value
 | 
			
		||||
    })
 | 
			
		||||
    list.value = data.list
 | 
			
		||||
    total.value = data.total
 | 
			
		||||
@@ -242,8 +306,8 @@ const handleQuery = () => {
 | 
			
		||||
  getList()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleTabChange = (tabName) => {
 | 
			
		||||
  type.value = tabName
 | 
			
		||||
const handleTabChange = (tabName: TabPaneName) => {
 | 
			
		||||
  msgType.value = tabName as MsgType
 | 
			
		||||
  handleQuery()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -252,94 +316,87 @@ const handleAdd = () => {
 | 
			
		||||
  reset()
 | 
			
		||||
  resetEditor()
 | 
			
		||||
  // 打开表单,并设置初始化
 | 
			
		||||
  open.value = true
 | 
			
		||||
  title.value = '新增自动回复'
 | 
			
		||||
  objData.value = {
 | 
			
		||||
    type: 'text',
 | 
			
		||||
    accountId: queryParams.accountId
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  title.value = '新增自动回复'
 | 
			
		||||
  showReplyFormDialog.value = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 修改按钮操作 */
 | 
			
		||||
const handleUpdate = (row) => {
 | 
			
		||||
const handleUpdate = async (row: any) => {
 | 
			
		||||
  reset()
 | 
			
		||||
  resetEditor()
 | 
			
		||||
  console.log(row)
 | 
			
		||||
 | 
			
		||||
  MpAutoReplyApi.getAutoReply(row.id).then((data) => {
 | 
			
		||||
    // 设置属性
 | 
			
		||||
    form.value = { ...data }
 | 
			
		||||
    delete form.value['responseMessageType']
 | 
			
		||||
    delete form.value['responseContent']
 | 
			
		||||
    delete form.value['responseMediaId']
 | 
			
		||||
    delete form.value['responseMediaUrl']
 | 
			
		||||
    delete form.value['responseDescription']
 | 
			
		||||
    delete form.value['responseArticles']
 | 
			
		||||
    objData.value = {
 | 
			
		||||
      type: data.responseMessageType,
 | 
			
		||||
      accountId: queryParams.accountId,
 | 
			
		||||
      content: data.responseContent,
 | 
			
		||||
      mediaId: data.responseMediaId,
 | 
			
		||||
      url: data.responseMediaUrl,
 | 
			
		||||
      title: data.responseTitle,
 | 
			
		||||
      description: data.responseDescription,
 | 
			
		||||
      thumbMediaId: data.responseThumbMediaId,
 | 
			
		||||
      thumbMediaUrl: data.responseThumbMediaUrl,
 | 
			
		||||
      articles: data.responseArticles,
 | 
			
		||||
      musicUrl: data.responseMusicUrl,
 | 
			
		||||
      hqMusicUrl: data.responseHqMusicUrl
 | 
			
		||||
    }
 | 
			
		||||
  const data = await MpAutoReplyApi.getAutoReply(row.id)
 | 
			
		||||
  // 设置属性
 | 
			
		||||
  replyForm.value = { ...data }
 | 
			
		||||
  delete replyForm.value['responseMessageType']
 | 
			
		||||
  delete replyForm.value['responseContent']
 | 
			
		||||
  delete replyForm.value['responseMediaId']
 | 
			
		||||
  delete replyForm.value['responseMediaUrl']
 | 
			
		||||
  delete replyForm.value['responseDescription']
 | 
			
		||||
  delete replyForm.value['responseArticles']
 | 
			
		||||
  objData.value = {
 | 
			
		||||
    type: data.responseMessageType,
 | 
			
		||||
    accountId: queryParams.accountId,
 | 
			
		||||
    content: data.responseContent,
 | 
			
		||||
    mediaId: data.responseMediaId,
 | 
			
		||||
    url: data.responseMediaUrl,
 | 
			
		||||
    title: data.responseTitle,
 | 
			
		||||
    description: data.responseDescription,
 | 
			
		||||
    thumbMediaId: data.responseThumbMediaId,
 | 
			
		||||
    thumbMediaUrl: data.responseThumbMediaUrl,
 | 
			
		||||
    articles: data.responseArticles,
 | 
			
		||||
    musicUrl: data.responseMusicUrl,
 | 
			
		||||
    hqMusicUrl: data.responseHqMusicUrl
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    // 打开表单
 | 
			
		||||
    open.value = true
 | 
			
		||||
    title.value = '修改自动回复'
 | 
			
		||||
  })
 | 
			
		||||
  // 打开表单
 | 
			
		||||
  title.value = '修改自动回复'
 | 
			
		||||
  showReplyFormDialog.value = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleSubmit = () => {
 | 
			
		||||
  formRef.value?.validate((valid) => {
 | 
			
		||||
    if (!valid) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
const handleSubmit = async () => {
 | 
			
		||||
  const valid = await formRef.value?.validate()
 | 
			
		||||
  if (!valid) return
 | 
			
		||||
 | 
			
		||||
    // 处理回复消息
 | 
			
		||||
    const form = { ...form.value }
 | 
			
		||||
    form.responseMessageType = objData.value.type
 | 
			
		||||
    form.responseContent = objData.value.content
 | 
			
		||||
    form.responseMediaId = objData.value.mediaId
 | 
			
		||||
    form.responseMediaUrl = objData.value.url
 | 
			
		||||
    form.responseTitle = objData.value.title
 | 
			
		||||
    form.responseDescription = objData.value.description
 | 
			
		||||
    form.responseThumbMediaId = objData.value.thumbMediaId
 | 
			
		||||
    form.responseThumbMediaUrl = objData.value.thumbMediaUrl
 | 
			
		||||
    form.responseArticles = objData.value.articles
 | 
			
		||||
    form.responseMusicUrl = objData.value.musicUrl
 | 
			
		||||
    form.responseHqMusicUrl = objData.value.hqMusicUrl
 | 
			
		||||
  // 处理回复消息
 | 
			
		||||
  const submitForm: any = { ...replyForm.value }
 | 
			
		||||
  submitForm.responseMessageType = objData.value.type
 | 
			
		||||
  submitForm.responseContent = objData.value.content
 | 
			
		||||
  submitForm.responseMediaId = objData.value.mediaId
 | 
			
		||||
  submitForm.responseMediaUrl = objData.value.url
 | 
			
		||||
  submitForm.responseTitle = objData.value.title
 | 
			
		||||
  submitForm.responseDescription = objData.value.description
 | 
			
		||||
  submitForm.responseThumbMediaId = objData.value.thumbMediaId
 | 
			
		||||
  submitForm.responseThumbMediaUrl = objData.value.thumbMediaUrl
 | 
			
		||||
  submitForm.responseArticles = objData.value.articles
 | 
			
		||||
  submitForm.responseMusicUrl = objData.value.musicUrl
 | 
			
		||||
  submitForm.responseHqMusicUrl = objData.value.hqMusicUrl
 | 
			
		||||
 | 
			
		||||
    if (form.value.id !== undefined) {
 | 
			
		||||
      MpAutoReplyApi.updateAutoReply(form).then(() => {
 | 
			
		||||
        message.success('修改成功')
 | 
			
		||||
        open.value = false
 | 
			
		||||
        getList()
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      MpAutoReplyApi.createAutoReply(form).then(() => {
 | 
			
		||||
        message.success('新增成功')
 | 
			
		||||
        open.value = false
 | 
			
		||||
        getList()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  if (replyForm.value.id !== undefined) {
 | 
			
		||||
    await MpAutoReplyApi.updateAutoReply(submitForm)
 | 
			
		||||
    message.success('修改成功')
 | 
			
		||||
  } else {
 | 
			
		||||
    await MpAutoReplyApi.createAutoReply(submitForm)
 | 
			
		||||
    message.success('新增成功')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  showReplyFormDialog.value = false
 | 
			
		||||
  getList()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 表单重置
 | 
			
		||||
const reset = () => {
 | 
			
		||||
  form.value = {
 | 
			
		||||
  replyForm.value = {
 | 
			
		||||
    id: undefined,
 | 
			
		||||
    accountId: queryParams.accountId,
 | 
			
		||||
    type: type.value,
 | 
			
		||||
    type: msgType.value,
 | 
			
		||||
    requestKeyword: undefined,
 | 
			
		||||
    requestMatch: type.value === '3' ? 1 : undefined,
 | 
			
		||||
    requestMatch: msgType.value === MsgType.Keyword ? 1 : undefined,
 | 
			
		||||
    requestMessageType: undefined
 | 
			
		||||
  }
 | 
			
		||||
  formRef.value?.resetFields()
 | 
			
		||||
@@ -347,7 +404,7 @@ const reset = () => {
 | 
			
		||||
 | 
			
		||||
// 取消按钮
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
  open.value = false
 | 
			
		||||
  showReplyFormDialog.value = false
 | 
			
		||||
  reset()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-select
 | 
			
		||||
    v-model="accountId"
 | 
			
		||||
    placeholder="请选择公众号"
 | 
			
		||||
    class="!w-240px"
 | 
			
		||||
    @change="accountChanged"
 | 
			
		||||
  >
 | 
			
		||||
  <el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged">
 | 
			
		||||
    <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
 | 
			
		||||
  </el-select>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<!-- TODO @芋艿:WxMpSelect 改成 WxAccountSelect,然后挪到现有的 wx-account-select 包下 -->
 | 
			
		||||
<script lang="ts" setup name="WxMpSelect">
 | 
			
		||||
import * as MpAccountApi from '@/api/mp/account'
 | 
			
		||||
 | 
			
		||||
const accountId: Ref<number | undefined> = ref()
 | 
			
		||||
const account: MpAccountApi.AccountVO = reactive({
 | 
			
		||||
  id: undefined,
 | 
			
		||||
  name: ''
 | 
			
		||||
})
 | 
			
		||||
const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: 'change', id: number | undefined): void
 | 
			
		||||
  (e: 'change', id?: number, name?: string): void
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
@@ -27,12 +25,12 @@ 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)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
<script name="WxEditor" setup>
 | 
			
		||||
import { reactive, ref } from 'vue'
 | 
			
		||||
import { QuillEditor } from '@vueup/vue-quill'
 | 
			
		||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
 | 
			
		||||
import { ref, reactive } from 'vue'
 | 
			
		||||
import { getAccessToken } from '@/utils/auth'
 | 
			
		||||
import editorOptions from './quill-options'
 | 
			
		||||
import { Editor } from '@/components/Editor'
 | 
			
		||||
 | 
			
		||||
const BASE_URL = import.meta.env.VITE_BASE_URL
 | 
			
		||||
const actionUrl = BASE_URL + '/admin-api/mp/material/upload-news-image' // 这里写你要上传的图片服务器地址
 | 
			
		||||
const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
 | 
			
		||||
 | 
			
		||||
const message = useMessage()
 | 
			
		||||
 | 
			
		||||
@@ -30,21 +30,16 @@ const props = defineProps({
 | 
			
		||||
const emit = defineEmits(['input'])
 | 
			
		||||
 | 
			
		||||
const myQuillEditorRef = ref()
 | 
			
		||||
 | 
			
		||||
const content = ref(props.value.replace(/data-src/g, 'src'))
 | 
			
		||||
 | 
			
		||||
const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
 | 
			
		||||
 | 
			
		||||
const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-news-image') // 这里写你要上传的图片服务器地址
 | 
			
		||||
const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部
 | 
			
		||||
const uploadData = reactive({
 | 
			
		||||
  type: 'image', // TODO 芋艿:试试要不要换成 thumb
 | 
			
		||||
  accountId: props.accountId
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const onEditorChange = () => {
 | 
			
		||||
const onEditorChange = (text) => {
 | 
			
		||||
  //内容改变事件
 | 
			
		||||
  emit('input', content.value)
 | 
			
		||||
  emit('input', text)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 富文本图片上传前
 | 
			
		||||
@@ -88,114 +83,22 @@ const uploadError = () => {
 | 
			
		||||
    <div v-loading="loading" element-loading-text="请稍等,图片上传中">
 | 
			
		||||
      <!-- 图片上传组件辅助-->
 | 
			
		||||
      <el-upload
 | 
			
		||||
        :action="actionUrl"
 | 
			
		||||
        :before-upload="beforeUpload"
 | 
			
		||||
        :data="uploadData"
 | 
			
		||||
        :headers="headers"
 | 
			
		||||
        :on-error="uploadError"
 | 
			
		||||
        :on-success="uploadSuccess"
 | 
			
		||||
        :show-file-list="false"
 | 
			
		||||
        class="avatar-uploader"
 | 
			
		||||
        name="file"
 | 
			
		||||
        :action="actionUrl"
 | 
			
		||||
        :headers="headers"
 | 
			
		||||
        :show-file-list="false"
 | 
			
		||||
        :data="uploadData"
 | 
			
		||||
        :on-success="uploadSuccess"
 | 
			
		||||
        :on-error="uploadError"
 | 
			
		||||
        :before-upload="beforeUpload"
 | 
			
		||||
      />
 | 
			
		||||
      <QuillEditor
 | 
			
		||||
      <Editor
 | 
			
		||||
        editor-id="wxEditor"
 | 
			
		||||
        ref="quillEditorRef"
 | 
			
		||||
        v-model="content"
 | 
			
		||||
        :options="editorOptions"
 | 
			
		||||
        class="editor"
 | 
			
		||||
        @change="onEditorChange($event)"
 | 
			
		||||
        :modelValue="content"
 | 
			
		||||
        @change="(editor) => onEditorChange(editor.getText())"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.editor {
 | 
			
		||||
  line-height: normal !important;
 | 
			
		||||
  height: 500px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-tooltip[data-mode='link']::before {
 | 
			
		||||
  content: '请输入链接地址:';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
 | 
			
		||||
  border-right: 0;
 | 
			
		||||
  content: '保存';
 | 
			
		||||
  padding-right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-tooltip[data-mode='video']::before {
 | 
			
		||||
  content: '请输入视频地址:';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
 | 
			
		||||
  content: '14px';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
 | 
			
		||||
  content: '10px';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
 | 
			
		||||
  content: '18px';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
 | 
			
		||||
  content: '32px';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
 | 
			
		||||
  content: '文本';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
 | 
			
		||||
  content: '标题1';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
 | 
			
		||||
  content: '标题2';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
 | 
			
		||||
  content: '标题3';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
 | 
			
		||||
  content: '标题4';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
 | 
			
		||||
  content: '标题5';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
 | 
			
		||||
  content: '标题6';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
 | 
			
		||||
  content: '标准字体';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
 | 
			
		||||
  content: '衬线字体';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
 | 
			
		||||
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
 | 
			
		||||
  content: '等宽字体';
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -39,79 +39,79 @@
 | 
			
		||||
              :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''"
 | 
			
		||||
            >
 | 
			
		||||
              <!-- 【事件】区域 -->
 | 
			
		||||
              <div v-if="item.type === 'event' && item.event === 'subscribe'">
 | 
			
		||||
              <div v-if="item.type === MsgType.Event && item.event === 'subscribe'">
 | 
			
		||||
                <el-tag type="success">关注</el-tag>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'unsubscribe'">
 | 
			
		||||
                <el-tag type="danger">取消关注</el-tag>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'CLICK'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'CLICK'">
 | 
			
		||||
                <el-tag>点击菜单</el-tag>
 | 
			
		||||
                【{{ item.eventKey }}】
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'VIEW'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'VIEW'">
 | 
			
		||||
                <el-tag>点击菜单链接</el-tag>
 | 
			
		||||
                【{{ item.eventKey }}】
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_waitmsg'">
 | 
			
		||||
                <el-tag>扫码结果</el-tag>
 | 
			
		||||
                【{{ item.eventKey }}】
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'scancode_push'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_push'">
 | 
			
		||||
                <el-tag>扫码结果</el-tag>
 | 
			
		||||
                【{{ item.eventKey }}】
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_sysphoto'">
 | 
			
		||||
                <el-tag>系统拍照发图</el-tag>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_photo_or_album'">
 | 
			
		||||
                <el-tag>拍照或者相册</el-tag>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'pic_weixin'">
 | 
			
		||||
                <el-tag>微信相册</el-tag>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event' && item.event === 'location_select'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event && item.event === 'location_select'">
 | 
			
		||||
                <el-tag>选择地理位置</el-tag>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'event'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Event">
 | 
			
		||||
                <el-tag type="danger">未知事件类型</el-tag>
 | 
			
		||||
              </div>
 | 
			
		||||
              <!-- 【消息】区域 -->
 | 
			
		||||
              <div v-else-if="item.type === 'text'">{{ item.content }}</div>
 | 
			
		||||
              <div v-else-if="item.type === 'voice'">
 | 
			
		||||
                <wx-voice-player :url="item.mediaUrl" :content="item.recognition" />
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Voice">
 | 
			
		||||
                <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'image'">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Image">
 | 
			
		||||
                <a target="_blank" :href="item.mediaUrl">
 | 
			
		||||
                  <img :src="item.mediaUrl" style="width: 100px" />
 | 
			
		||||
                </a>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div
 | 
			
		||||
                v-else-if="item.type === 'video' || item.type === 'shortvideo'"
 | 
			
		||||
                v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'"
 | 
			
		||||
                style="text-align: center"
 | 
			
		||||
              >
 | 
			
		||||
                <wx-video-player :url="item.mediaUrl" />
 | 
			
		||||
                <WxVideoPlayer :url="item.mediaUrl" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'link'" class="avue-card__detail">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Link" class="avue-card__detail">
 | 
			
		||||
                <el-link type="success" :underline="false" target="_blank" :href="item.url">
 | 
			
		||||
                  <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
 | 
			
		||||
                </el-link>
 | 
			
		||||
                <div class="avue-card__info" style="height: unset">{{ item.description }}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <!-- TODO 芋艿:待完善 -->
 | 
			
		||||
              <div v-else-if="item.type === 'location'">
 | 
			
		||||
                <wx-location
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Location">
 | 
			
		||||
                <WxLocation
 | 
			
		||||
                  :label="item.label"
 | 
			
		||||
                  :location-y="item.locationY"
 | 
			
		||||
                  :location-x="item.locationX"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'news'" style="width: 300px">
 | 
			
		||||
              <div v-else-if="item.type === MsgType.News" style="width: 300px">
 | 
			
		||||
                <!-- TODO 芋艿:待测试;详情页也存在类似的情况 -->
 | 
			
		||||
                <wx-news :articles="item.articles" />
 | 
			
		||||
                <WxNews :articles="item.articles" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div v-else-if="item.type === 'music'">
 | 
			
		||||
                <wx-music
 | 
			
		||||
              <div v-else-if="item.type === MsgType.Music">
 | 
			
		||||
                <WxMusic
 | 
			
		||||
                  :title="item.title"
 | 
			
		||||
                  :description="item.description"
 | 
			
		||||
                  :thumb-media-url="item.thumbMediaUrl"
 | 
			
		||||
@@ -125,182 +125,185 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="msg-send" v-loading="sendLoading">
 | 
			
		||||
      <wx-reply-select ref="replySelect" :objData="objData" />
 | 
			
		||||
      <WxReplySelect ref="replySelectRef" :objData="objData" />
 | 
			
		||||
      <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" name="WxMsg">
 | 
			
		||||
import { getMessagePage, sendMessage } from '@/api/mp/message'
 | 
			
		||||
<script setup lang="ts" name="WxMsg">
 | 
			
		||||
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
 | 
			
		||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
 | 
			
		||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
 | 
			
		||||
import WxNews from '@/views/mp/components/wx-news/main.vue'
 | 
			
		||||
import WxLocation from '@/views/mp/components/wx-location/main.vue'
 | 
			
		||||
import WxMusic from '@/views/mp/components/wx-music/main.vue'
 | 
			
		||||
import { getMessagePage, sendMessage } from '@/api/mp/message'
 | 
			
		||||
import { getUser } from '@/api/mp/user'
 | 
			
		||||
import { defineComponent } from 'vue'
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
import { formatDate } from '@/utils/formatTime'
 | 
			
		||||
import profile from '@/assets/imgs/profile.jpg'
 | 
			
		||||
import wechat from '@/assets/imgs/wechat.png'
 | 
			
		||||
import { formatDate } from '@/utils/formatTime'
 | 
			
		||||
import { MsgType } from './types'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: {
 | 
			
		||||
    WxReplySelect,
 | 
			
		||||
    WxVideoPlayer,
 | 
			
		||||
    WxVoicePlayer,
 | 
			
		||||
    WxNews,
 | 
			
		||||
    WxLocation,
 | 
			
		||||
    WxMusic
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    userId: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      required: true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
 | 
			
		||||
    const loading = ref(false) // 消息列表是否正在加载中
 | 
			
		||||
    const loadMore = ref(true) // 是否可以加载更多
 | 
			
		||||
    const list = ref<any[]>([]) // 消息列表
 | 
			
		||||
    const queryParams = reactive({
 | 
			
		||||
      pageNo: 1, // 当前页数
 | 
			
		||||
      pageSize: 14, // 每页显示多少条
 | 
			
		||||
      accountId: undefined
 | 
			
		||||
    })
 | 
			
		||||
    const user = reactive({
 | 
			
		||||
      // 由于微信不再提供昵称,直接使用“用户”展示
 | 
			
		||||
      nickname: '用户',
 | 
			
		||||
      avatar: profile,
 | 
			
		||||
      accountId: 0 // 公众号账号编号
 | 
			
		||||
    })
 | 
			
		||||
    const mp = reactive({
 | 
			
		||||
      nickname: '公众号',
 | 
			
		||||
      avatar: wechat
 | 
			
		||||
    })
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
 | 
			
		||||
    // ========= 消息发送 =========
 | 
			
		||||
    const sendLoading = ref(false) // 发送消息是否加载中
 | 
			
		||||
    const objData = reactive({
 | 
			
		||||
      // 微信发送消息
 | 
			
		||||
      type: 'text',
 | 
			
		||||
      accountId: null,
 | 
			
		||||
      articles: []
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const replySelect = ref(null)
 | 
			
		||||
    // 执行发送
 | 
			
		||||
    const sendMsg = async () => {
 | 
			
		||||
      if (!objData) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      //     // 公众号限制:客服消息,公众号只允许发送一条
 | 
			
		||||
      if (objData.type === 'news' && objData.articles.length > 1) {
 | 
			
		||||
        objData.articles = [objData.articles[0]]
 | 
			
		||||
        message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
 | 
			
		||||
      }
 | 
			
		||||
      let data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
 | 
			
		||||
      sendLoading.value = false
 | 
			
		||||
      list.value = [...list.value, ...[data]]
 | 
			
		||||
      scrollToBottom()
 | 
			
		||||
      //ts檢查的時候會判斷這個組件可能是空的,所以需要進行斷言。
 | 
			
		||||
      //避免 tab 的数据未清理
 | 
			
		||||
      const deleteObj = (replySelect.value as any).deleteObj
 | 
			
		||||
      if (deleteObj) {
 | 
			
		||||
        deleteObj()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const loadingMore = () => {
 | 
			
		||||
      queryParams.pageNo++
 | 
			
		||||
      getPage(queryParams, null)
 | 
			
		||||
    }
 | 
			
		||||
    const getPage = async (page, params) => {
 | 
			
		||||
      loading.value = true
 | 
			
		||||
      let dataTemp = await getMessagePage(
 | 
			
		||||
        Object.assign(
 | 
			
		||||
          {
 | 
			
		||||
            pageNo: page.pageNo,
 | 
			
		||||
            pageSize: page.pageSize,
 | 
			
		||||
            userId: props.userId,
 | 
			
		||||
            accountId: page.accountId
 | 
			
		||||
          },
 | 
			
		||||
          params
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
      const msgDiv = document.getElementById('msg-div' + nowStr.value)
 | 
			
		||||
      let scrollHeight = 0
 | 
			
		||||
      if (msgDiv) {
 | 
			
		||||
        scrollHeight = msgDiv.scrollHeight
 | 
			
		||||
      }
 | 
			
		||||
      // 处理数据
 | 
			
		||||
      let data = dataTemp.list.reverse()
 | 
			
		||||
      list.value = [...data, ...list.value]
 | 
			
		||||
      loading.value = false
 | 
			
		||||
      if (data.length < queryParams.pageSize || data.length === 0) {
 | 
			
		||||
        loadMore.value = false
 | 
			
		||||
      }
 | 
			
		||||
      queryParams.pageNo = page.pageNo
 | 
			
		||||
      queryParams.pageSize = page.pageSize
 | 
			
		||||
      // 滚动到原来的位置
 | 
			
		||||
      if (queryParams.pageNo === 1) {
 | 
			
		||||
        // 定位到消息底部
 | 
			
		||||
        scrollToBottom()
 | 
			
		||||
      } else if (data.length !== 0) {
 | 
			
		||||
        // 定位滚动条
 | 
			
		||||
        await nextTick(() => {
 | 
			
		||||
          if (scrollHeight !== 0) {
 | 
			
		||||
            let div = document.getElementById('msg-div' + nowStr.value)
 | 
			
		||||
            if (div && msgDiv) {
 | 
			
		||||
              msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const refreshChange = () => {
 | 
			
		||||
      getPage(queryParams, null)
 | 
			
		||||
    }
 | 
			
		||||
    /** 定位到消息底部 */
 | 
			
		||||
    const scrollToBottom = () => {
 | 
			
		||||
      nextTick(() => {
 | 
			
		||||
        let div = document.getElementById('msg-div' + nowStr.value)
 | 
			
		||||
        if (div) {
 | 
			
		||||
          div.scrollTop = div.scrollHeight
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMounted(async () => {
 | 
			
		||||
      let data = await getUser(props.userId)
 | 
			
		||||
      user.nickname = data.nickname && data.nickname.length > 0 ? data.nickname : user.nickname
 | 
			
		||||
      user.avatar = data.avatar && user.avatar.length > 0 ? data.avatar : user.avatar
 | 
			
		||||
      user.accountId = data.accountId
 | 
			
		||||
      queryParams.accountId = data.accountId
 | 
			
		||||
      objData.accountId = data.accountId
 | 
			
		||||
      refreshChange()
 | 
			
		||||
    })
 | 
			
		||||
    return {
 | 
			
		||||
      sendMsg,
 | 
			
		||||
      loadingMore,
 | 
			
		||||
      formatDate,
 | 
			
		||||
      scrollToBottom,
 | 
			
		||||
      objData,
 | 
			
		||||
      mp,
 | 
			
		||||
      user,
 | 
			
		||||
      queryParams,
 | 
			
		||||
      list,
 | 
			
		||||
      loadMore,
 | 
			
		||||
      loading,
 | 
			
		||||
      nowStr,
 | 
			
		||||
      sendLoading
 | 
			
		||||
    }
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  userId: {
 | 
			
		||||
    type: Number,
 | 
			
		||||
    required: true
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
 | 
			
		||||
const loading = ref(false) // 消息列表是否正在加载中
 | 
			
		||||
const loadMore = ref(true) // 是否可以加载更多
 | 
			
		||||
const list = ref<any[]>([]) // 消息列表
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
  pageNo: 1, // 当前页数
 | 
			
		||||
  pageSize: 14, // 每页显示多少条
 | 
			
		||||
  accountId: undefined
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
interface User {
 | 
			
		||||
  nickname: string
 | 
			
		||||
  avatar: string
 | 
			
		||||
  accountId: number
 | 
			
		||||
}
 | 
			
		||||
// 由于微信不再提供昵称,直接使用“用户”展示
 | 
			
		||||
const user: User = reactive({
 | 
			
		||||
  nickname: '用户',
 | 
			
		||||
  avatar: profile,
 | 
			
		||||
  accountId: 0 // 公众号账号编号
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
interface Mp {
 | 
			
		||||
  nickname: string
 | 
			
		||||
  avatar: string
 | 
			
		||||
}
 | 
			
		||||
const mp: Mp = reactive({
 | 
			
		||||
  nickname: '公众号',
 | 
			
		||||
  avatar: wechat
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// ========= 消息发送 =========
 | 
			
		||||
const sendLoading = ref(false) // 发送消息是否加载中
 | 
			
		||||
interface ObjData {
 | 
			
		||||
  type: MsgType
 | 
			
		||||
  accountId: number | null
 | 
			
		||||
  articles: any[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 微信发送消息
 | 
			
		||||
const objData: ObjData = reactive({
 | 
			
		||||
  type: MsgType.Text,
 | 
			
		||||
  accountId: null,
 | 
			
		||||
  articles: []
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null)
 | 
			
		||||
 | 
			
		||||
/** 完成加载 */
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  const data = await getUser(props.userId)
 | 
			
		||||
  user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname
 | 
			
		||||
  user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar
 | 
			
		||||
  user.accountId = data.accountId
 | 
			
		||||
  queryParams.accountId = data.accountId
 | 
			
		||||
  objData.accountId = data.accountId
 | 
			
		||||
 | 
			
		||||
  refreshChange()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 执行发送
 | 
			
		||||
const sendMsg = async () => {
 | 
			
		||||
  if (!objData) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  // 公众号限制:客服消息,公众号只允许发送一条
 | 
			
		||||
  if (objData.type === MsgType.News && objData.articles.length > 1) {
 | 
			
		||||
    objData.articles = [objData.articles[0]]
 | 
			
		||||
    message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
 | 
			
		||||
  sendLoading.value = false
 | 
			
		||||
 | 
			
		||||
  list.value = [...list.value, ...[data]]
 | 
			
		||||
  scrollToBottom()
 | 
			
		||||
 | 
			
		||||
  //ts检查的時候会判断这个组件可能是空的,所以需要进行断言。
 | 
			
		||||
  //避免 tab 的数据未清理
 | 
			
		||||
  const deleteObj = replySelectRef.value?.deleteObj
 | 
			
		||||
  if (deleteObj) {
 | 
			
		||||
    deleteObj()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const loadingMore = () => {
 | 
			
		||||
  queryParams.pageNo++
 | 
			
		||||
  getPage(queryParams, null)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getPage = async (page, params) => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  let dataTemp = await getMessagePage(
 | 
			
		||||
    Object.assign(
 | 
			
		||||
      {
 | 
			
		||||
        pageNo: page.pageNo,
 | 
			
		||||
        pageSize: page.pageSize,
 | 
			
		||||
        userId: props.userId,
 | 
			
		||||
        accountId: page.accountId
 | 
			
		||||
      },
 | 
			
		||||
      params
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const msgDiv = document.getElementById('msg-div' + nowStr.value)
 | 
			
		||||
  let scrollHeight = 0
 | 
			
		||||
  if (msgDiv) {
 | 
			
		||||
    scrollHeight = msgDiv.scrollHeight
 | 
			
		||||
  }
 | 
			
		||||
  // 处理数据
 | 
			
		||||
  const data = dataTemp.list.reverse()
 | 
			
		||||
  list.value = [...data, ...list.value]
 | 
			
		||||
  loading.value = false
 | 
			
		||||
  if (data.length < queryParams.pageSize || data.length === 0) {
 | 
			
		||||
    loadMore.value = false
 | 
			
		||||
  }
 | 
			
		||||
  queryParams.pageNo = page.pageNo
 | 
			
		||||
  queryParams.pageSize = page.pageSize
 | 
			
		||||
  // 滚动到原来的位置
 | 
			
		||||
  if (queryParams.pageNo === 1) {
 | 
			
		||||
    // 定位到消息底部
 | 
			
		||||
    scrollToBottom()
 | 
			
		||||
  } else if (data.length !== 0) {
 | 
			
		||||
    // 定位滚动条
 | 
			
		||||
    await nextTick(() => {
 | 
			
		||||
      if (scrollHeight !== 0) {
 | 
			
		||||
        let div = document.getElementById('msg-div' + nowStr.value)
 | 
			
		||||
        if (div && msgDiv) {
 | 
			
		||||
          msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const refreshChange = () => {
 | 
			
		||||
  getPage(queryParams, null)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 定位到消息底部 */
 | 
			
		||||
const scrollToBottom = () => {
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    let div = document.getElementById('msg-div' + nowStr.value)
 | 
			
		||||
    if (div) {
 | 
			
		||||
      div.scrollTop = div.scrollHeight
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc  */
 | 
			
		||||
@import './comment.scss';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								src/views/mp/components/wx-msg/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/views/mp/components/wx-msg/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
export enum MsgType {
 | 
			
		||||
  Event = 'event',
 | 
			
		||||
  Text = 'text',
 | 
			
		||||
  Voice = 'voice',
 | 
			
		||||
  Image = 'image',
 | 
			
		||||
  Video = 'video',
 | 
			
		||||
  Link = 'link',
 | 
			
		||||
  Location = 'location',
 | 
			
		||||
  Music = 'music',
 | 
			
		||||
  News = 'news'
 | 
			
		||||
}
 | 
			
		||||
@@ -3,14 +3,22 @@
 | 
			
		||||
 | 
			
		||||
  <!-- 搜索工作栏 -->
 | 
			
		||||
  <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">
 | 
			
		||||
        <WxMpSelect @change="onAccountChanged" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item>
 | 
			
		||||
        <el-button type="primary" plain @click="handleAdd" v-hasPermi="['mp:draft:create']">
 | 
			
		||||
          <Icon icon="ep:plus" />新增
 | 
			
		||||
        </el-button>
 | 
			
		||||
      </template>
 | 
			
		||||
    </WxAccountSelect>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <!-- 列表 -->
 | 
			
		||||
@@ -58,214 +66,220 @@
 | 
			
		||||
    />
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <!-- TODO @Dhb52:迁移成独立路由 -->
 | 
			
		||||
  <div class="app-container">
 | 
			
		||||
    <!-- 添加或修改草稿对话框 -->
 | 
			
		||||
    <Teleport to="body">
 | 
			
		||||
      <el-dialog
 | 
			
		||||
        :title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
 | 
			
		||||
        width="80%"
 | 
			
		||||
        top="20px"
 | 
			
		||||
        v-model="dialogNewsVisible"
 | 
			
		||||
        :before-close="dialogNewsClose"
 | 
			
		||||
        :close-on-click-modal="false"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="left">
 | 
			
		||||
          <div class="select-item">
 | 
			
		||||
            <div v-for="(news, index) in articlesAdd" :key="news.id">
 | 
			
		||||
              <div
 | 
			
		||||
                class="news-main father"
 | 
			
		||||
                v-if="index === 0"
 | 
			
		||||
                :class="{ activeAddNews: isActiveAddNews === index }"
 | 
			
		||||
                @click="activeNews(index)"
 | 
			
		||||
              >
 | 
			
		||||
                <div class="news-content">
 | 
			
		||||
                  <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" />
 | 
			
		||||
                  <div class="news-content-title">{{ news.title }}</div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="child" v-if="articlesAdd.length > 1">
 | 
			
		||||
                  <el-button size="small" @click="downNews(index)"
 | 
			
		||||
                    ><Icon icon="ep:sort-down" />下移</el-button
 | 
			
		||||
                  >
 | 
			
		||||
                  <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)"
 | 
			
		||||
                    ><Icon icon="ep:delete" />删除
 | 
			
		||||
                  </el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
    <el-dialog
 | 
			
		||||
      :title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
 | 
			
		||||
      width="80%"
 | 
			
		||||
      top="20px"
 | 
			
		||||
      v-model="dialogNewsVisible"
 | 
			
		||||
      :before-close="dialogNewsClose"
 | 
			
		||||
      :close-on-click-modal="false"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="left">
 | 
			
		||||
        <div class="select-item">
 | 
			
		||||
          <div v-for="(news, index) in articlesAdd" :key="news.id">
 | 
			
		||||
            <div
 | 
			
		||||
              class="news-main father"
 | 
			
		||||
              v-if="index === 0"
 | 
			
		||||
              :class="{ activeAddNews: isActiveAddNews === index }"
 | 
			
		||||
              @click="activeNews(index)"
 | 
			
		||||
            >
 | 
			
		||||
              <div class="news-content">
 | 
			
		||||
                <img class="material-img" v-if="news.thumbUrl" :src="news.thumbUrl" />
 | 
			
		||||
                <div class="news-content-title">{{ news.title }}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div
 | 
			
		||||
                class="news-main-item father"
 | 
			
		||||
                v-if="index > 0"
 | 
			
		||||
                :class="{ activeAddNews: isActiveAddNews === index }"
 | 
			
		||||
                @click="activeNews(index)"
 | 
			
		||||
              >
 | 
			
		||||
                <div class="news-content-item">
 | 
			
		||||
                  <div class="news-content-item-title">{{ news.title }}</div>
 | 
			
		||||
                  <div class="news-content-item-img">
 | 
			
		||||
                    <img
 | 
			
		||||
                      class="material-img"
 | 
			
		||||
                      v-if="news.thumbUrl"
 | 
			
		||||
                      :src="news.thumbUrl"
 | 
			
		||||
                      height="100%"
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="child">
 | 
			
		||||
                  <el-button
 | 
			
		||||
                    v-if="articlesAdd.length > index + 1"
 | 
			
		||||
                    size="small"
 | 
			
		||||
                    @click="downNews(index)"
 | 
			
		||||
                    ><Icon icon="ep:sort-down" />下移
 | 
			
		||||
                  </el-button>
 | 
			
		||||
                  <el-button size="small" @click="upNews(index)"
 | 
			
		||||
                    ><Icon icon="ep:sort-up" />上移</el-button
 | 
			
		||||
                  >
 | 
			
		||||
                  <el-button
 | 
			
		||||
                    v-if="operateMaterial === 'add'"
 | 
			
		||||
                    type="danger"
 | 
			
		||||
                    size="small"
 | 
			
		||||
                    @click="minusNews(index)"
 | 
			
		||||
                    ><Icon icon="ep:delete" />删除
 | 
			
		||||
                  </el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <el-row justify="center" class="ope-row">
 | 
			
		||||
              <el-button
 | 
			
		||||
                type="primary"
 | 
			
		||||
                circle
 | 
			
		||||
                @click="plusNews(item)"
 | 
			
		||||
                v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
 | 
			
		||||
              >
 | 
			
		||||
                <Icon icon="ep:plus" />
 | 
			
		||||
              </el-button>
 | 
			
		||||
            </el-row>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
 | 
			
		||||
          <br />
 | 
			
		||||
          <br />
 | 
			
		||||
          <br />
 | 
			
		||||
          <br />
 | 
			
		||||
          <!-- 标题、作者、原文地址 -->
 | 
			
		||||
          <el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题(必填)" />
 | 
			
		||||
          <el-input
 | 
			
		||||
            v-model="articlesAdd[isActiveAddNews].author"
 | 
			
		||||
            placeholder="请输入作者"
 | 
			
		||||
            style="margin-top: 5px"
 | 
			
		||||
          />
 | 
			
		||||
          <el-input
 | 
			
		||||
            v-model="articlesAdd[isActiveAddNews].contentSourceUrl"
 | 
			
		||||
            placeholder="请输入原文地址"
 | 
			
		||||
            style="margin-top: 5px"
 | 
			
		||||
          />
 | 
			
		||||
          <!-- 封面和摘要 -->
 | 
			
		||||
          <div class="input-tt">封面和摘要:</div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <div class="thumb-div">
 | 
			
		||||
              <img
 | 
			
		||||
                class="material-img"
 | 
			
		||||
                v-if="articlesAdd[isActiveAddNews].thumbUrl"
 | 
			
		||||
                :src="articlesAdd[isActiveAddNews].thumbUrl"
 | 
			
		||||
                :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
 | 
			
		||||
              />
 | 
			
		||||
              <Icon
 | 
			
		||||
                v-else
 | 
			
		||||
                icon="ep:plus"
 | 
			
		||||
                class="avatar-uploader-icon"
 | 
			
		||||
                :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
 | 
			
		||||
              />
 | 
			
		||||
              <div class="thumb-but">
 | 
			
		||||
                <el-upload
 | 
			
		||||
                  :action="actionUrl"
 | 
			
		||||
                  :headers="headers"
 | 
			
		||||
                  multiple
 | 
			
		||||
                  :limit="1"
 | 
			
		||||
                  :file-list="fileList"
 | 
			
		||||
                  :data="uploadData"
 | 
			
		||||
                  :before-upload="beforeThumbImageUpload"
 | 
			
		||||
                  :on-success="handleUploadSuccess"
 | 
			
		||||
              <div class="child" v-if="articlesAdd.length > 1">
 | 
			
		||||
                <el-button size="small" @click="downNews(index)"
 | 
			
		||||
                  ><Icon icon="ep:sort-down" />下移</el-button
 | 
			
		||||
                >
 | 
			
		||||
                  <template #trigger>
 | 
			
		||||
                    <el-button size="small" type="primary">本地上传</el-button>
 | 
			
		||||
                  </template>
 | 
			
		||||
                  <el-button
 | 
			
		||||
                    size="small"
 | 
			
		||||
                    type="primary"
 | 
			
		||||
                    @click="openMaterial"
 | 
			
		||||
                    style="margin-left: 5px"
 | 
			
		||||
                    >素材库选择</el-button
 | 
			
		||||
                  >
 | 
			
		||||
                  <template #tip>
 | 
			
		||||
                    <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div>
 | 
			
		||||
                  </template>
 | 
			
		||||
                </el-upload>
 | 
			
		||||
                <el-button v-if="operateMaterial === 'add'" size="small" @click="minusNews(index)"
 | 
			
		||||
                  ><Icon icon="ep:delete" />删除
 | 
			
		||||
                </el-button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div
 | 
			
		||||
              class="news-main-item father"
 | 
			
		||||
              v-if="index > 0"
 | 
			
		||||
              :class="{ activeAddNews: isActiveAddNews === index }"
 | 
			
		||||
              @click="activeNews(index)"
 | 
			
		||||
            >
 | 
			
		||||
              <div class="news-content-item">
 | 
			
		||||
                <div class="news-content-item-title">{{ news.title }}</div>
 | 
			
		||||
                <div class="news-content-item-img">
 | 
			
		||||
                  <img
 | 
			
		||||
                    class="material-img"
 | 
			
		||||
                    v-if="news.thumbUrl"
 | 
			
		||||
                    :src="news.thumbUrl"
 | 
			
		||||
                    height="100%"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="child">
 | 
			
		||||
                <el-button
 | 
			
		||||
                  v-if="articlesAdd.length > index + 1"
 | 
			
		||||
                  size="small"
 | 
			
		||||
                  @click="downNews(index)"
 | 
			
		||||
                  ><Icon icon="ep:sort-down" />下移
 | 
			
		||||
                </el-button>
 | 
			
		||||
                <el-button size="small" @click="upNews(index)"
 | 
			
		||||
                  ><Icon icon="ep:sort-up" />上移</el-button
 | 
			
		||||
                >
 | 
			
		||||
                <el-button
 | 
			
		||||
                  v-if="operateMaterial === 'add'"
 | 
			
		||||
                  type="danger"
 | 
			
		||||
                  size="small"
 | 
			
		||||
                  @click="minusNews(index)"
 | 
			
		||||
                  ><Icon icon="ep:delete" />删除
 | 
			
		||||
                </el-button>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Teleport to="body">
 | 
			
		||||
                <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%">
 | 
			
		||||
                  <WxMaterialSelect
 | 
			
		||||
                    ref="materialSelectRef"
 | 
			
		||||
                    :objData="{ type: 'image', accountId: queryParams.accountId }"
 | 
			
		||||
                    @select-material="selectMaterial"
 | 
			
		||||
                  />
 | 
			
		||||
                </el-dialog>
 | 
			
		||||
              </Teleport>
 | 
			
		||||
            </div>
 | 
			
		||||
            <el-input
 | 
			
		||||
              :rows="8"
 | 
			
		||||
              type="textarea"
 | 
			
		||||
              v-model="articlesAdd[isActiveAddNews].digest"
 | 
			
		||||
              placeholder="请输入摘要"
 | 
			
		||||
              class="digest"
 | 
			
		||||
              maxlength="120"
 | 
			
		||||
              style="float: right"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <!--富文本编辑器组件-->
 | 
			
		||||
          <el-row>
 | 
			
		||||
            <WxEditor
 | 
			
		||||
              v-model="articlesAdd[isActiveAddNews].content"
 | 
			
		||||
              :account-id="uploadData.accountId"
 | 
			
		||||
              v-if="hackResetEditor"
 | 
			
		||||
            />
 | 
			
		||||
          <el-row justify="center" class="ope-row">
 | 
			
		||||
            <el-button
 | 
			
		||||
              type="primary"
 | 
			
		||||
              circle
 | 
			
		||||
              @click="plusNews"
 | 
			
		||||
              v-if="articlesAdd.length < 8 && operateMaterial === 'add'"
 | 
			
		||||
            >
 | 
			
		||||
              <Icon icon="ep:plus" />
 | 
			
		||||
            </el-button>
 | 
			
		||||
          </el-row>
 | 
			
		||||
        </div>
 | 
			
		||||
        <template #footer>
 | 
			
		||||
          <el-button @click="dialogNewsVisible = false">取 消</el-button>
 | 
			
		||||
          <el-button type="primary" @click="submitForm">提 交</el-button>
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-dialog>
 | 
			
		||||
    </Teleport>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="right" v-loading="addMaterialLoading" v-if="articlesAdd.length > 0">
 | 
			
		||||
        <br />
 | 
			
		||||
        <br />
 | 
			
		||||
        <br />
 | 
			
		||||
        <br />
 | 
			
		||||
        <!-- 标题、作者、原文地址 -->
 | 
			
		||||
        <el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题(必填)" />
 | 
			
		||||
        <el-input
 | 
			
		||||
          v-model="articlesAdd[isActiveAddNews].author"
 | 
			
		||||
          placeholder="请输入作者"
 | 
			
		||||
          style="margin-top: 5px"
 | 
			
		||||
        />
 | 
			
		||||
        <el-input
 | 
			
		||||
          v-model="articlesAdd[isActiveAddNews].contentSourceUrl"
 | 
			
		||||
          placeholder="请输入原文地址"
 | 
			
		||||
          style="margin-top: 5px"
 | 
			
		||||
        />
 | 
			
		||||
        <!-- 封面和摘要 -->
 | 
			
		||||
        <div class="input-tt">封面和摘要:</div>
 | 
			
		||||
        <div>
 | 
			
		||||
          <div class="thumb-div">
 | 
			
		||||
            <img
 | 
			
		||||
              class="material-img"
 | 
			
		||||
              v-if="articlesAdd[isActiveAddNews].thumbUrl"
 | 
			
		||||
              :src="articlesAdd[isActiveAddNews].thumbUrl"
 | 
			
		||||
              :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
 | 
			
		||||
            />
 | 
			
		||||
            <Icon
 | 
			
		||||
              v-else
 | 
			
		||||
              icon="ep:plus"
 | 
			
		||||
              class="avatar-uploader-icon"
 | 
			
		||||
              :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
 | 
			
		||||
            />
 | 
			
		||||
            <div class="thumb-but">
 | 
			
		||||
              <el-upload
 | 
			
		||||
                :action="uploadUrl"
 | 
			
		||||
                :headers="headers"
 | 
			
		||||
                multiple
 | 
			
		||||
                :limit="1"
 | 
			
		||||
                :file-list="fileList"
 | 
			
		||||
                :data="uploadData"
 | 
			
		||||
                :before-upload="beforeThumbImageUpload"
 | 
			
		||||
                :on-success="handleUploadSuccess"
 | 
			
		||||
              >
 | 
			
		||||
                <template #trigger>
 | 
			
		||||
                  <el-button size="small" type="primary">本地上传</el-button>
 | 
			
		||||
                </template>
 | 
			
		||||
                <el-button
 | 
			
		||||
                  size="small"
 | 
			
		||||
                  type="primary"
 | 
			
		||||
                  @click="openMaterial"
 | 
			
		||||
                  style="margin-left: 5px"
 | 
			
		||||
                  >素材库选择</el-button
 | 
			
		||||
                >
 | 
			
		||||
                <template #tip>
 | 
			
		||||
                  <div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div>
 | 
			
		||||
                </template>
 | 
			
		||||
              </el-upload>
 | 
			
		||||
            </div>
 | 
			
		||||
            <el-dialog title="选择图片" v-model="dialogImageVisible" width="80%" append-to-body>
 | 
			
		||||
              <WxMaterialSelect
 | 
			
		||||
                ref="materialSelectRef"
 | 
			
		||||
                :objData="{ type: 'image', accountId: queryParams.accountId }"
 | 
			
		||||
                @select-material="selectMaterial"
 | 
			
		||||
              />
 | 
			
		||||
            </el-dialog>
 | 
			
		||||
          </div>
 | 
			
		||||
          <el-input
 | 
			
		||||
            :rows="8"
 | 
			
		||||
            type="textarea"
 | 
			
		||||
            v-model="articlesAdd[isActiveAddNews].digest"
 | 
			
		||||
            placeholder="请输入摘要"
 | 
			
		||||
            class="digest"
 | 
			
		||||
            maxlength="120"
 | 
			
		||||
            style="float: right"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <!--富文本编辑器组件-->
 | 
			
		||||
        <el-row>
 | 
			
		||||
          <WxEditor
 | 
			
		||||
            v-model="articlesAdd[isActiveAddNews].content"
 | 
			
		||||
            :account-id="uploadData.accountId"
 | 
			
		||||
            v-if="hackResetEditor"
 | 
			
		||||
          />
 | 
			
		||||
        </el-row>
 | 
			
		||||
      </div>
 | 
			
		||||
      <template #footer>
 | 
			
		||||
        <el-button @click="dialogNewsVisible = false">取 消</el-button>
 | 
			
		||||
        <el-button type="primary" @click="submitForm">提 交</el-button>
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup name="MpDraft">
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts" name="MpDraft">
 | 
			
		||||
import WxEditor from '@/views/mp/components/wx-editor/WxEditor.vue'
 | 
			
		||||
import WxNews from '@/views/mp/components/wx-news/main.vue'
 | 
			
		||||
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'
 | 
			
		||||
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue'
 | 
			
		||||
import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 | 
			
		||||
import { getAccessToken } from '@/utils/auth'
 | 
			
		||||
import * as MpDraftApi from '@/api/mp/draft'
 | 
			
		||||
import * as MpFreePublishApi from '@/api/mp/freePublish'
 | 
			
		||||
import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
 | 
			
		||||
// 可以用改本地数据模拟,避免API调用超限
 | 
			
		||||
// import drafts from './mock'
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息
 | 
			
		||||
 | 
			
		||||
const loading = ref(true) // 列表的加载中
 | 
			
		||||
const list = ref<any[]>([]) // 列表的数据
 | 
			
		||||
const total = ref(0) // 列表的总页数
 | 
			
		||||
const list = ref([]) // 列表的数据
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
interface QueryParams {
 | 
			
		||||
  pageNo: number
 | 
			
		||||
  pageSize: number
 | 
			
		||||
  accountId?: number
 | 
			
		||||
}
 | 
			
		||||
const queryParams: QueryParams = reactive({
 | 
			
		||||
  pageNo: 1,
 | 
			
		||||
  pageSize: 10,
 | 
			
		||||
  accountId: undefined
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// ========== 文件上传 ==========
 | 
			
		||||
const materialSelectRef = ref()
 | 
			
		||||
const BASE_URL = import.meta.env.VITE_BASE_URL
 | 
			
		||||
const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-permanent') // 上传永久素材的地址
 | 
			
		||||
const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部
 | 
			
		||||
const fileList = ref([])
 | 
			
		||||
const uploadData = reactive({
 | 
			
		||||
const uploadUrl = BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传永久素材的地址
 | 
			
		||||
const headers = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
 | 
			
		||||
 | 
			
		||||
const materialSelectRef = ref<InstanceType<typeof WxMaterialSelect> | null>(null)
 | 
			
		||||
const fileList = ref<UploadFiles>([])
 | 
			
		||||
interface UploadData {
 | 
			
		||||
  type: 'image' | 'video' | 'audio'
 | 
			
		||||
  accountId?: number
 | 
			
		||||
}
 | 
			
		||||
const uploadData: UploadData = reactive({
 | 
			
		||||
  type: 'image',
 | 
			
		||||
  accountId: 1
 | 
			
		||||
})
 | 
			
		||||
@@ -273,34 +287,28 @@ const uploadData = reactive({
 | 
			
		||||
// ========== 草稿新建 or 修改 ==========
 | 
			
		||||
const dialogNewsVisible = ref(false)
 | 
			
		||||
const addMaterialLoading = ref(false) // 添加草稿的 loading 标识
 | 
			
		||||
const articlesAdd = ref([])
 | 
			
		||||
const articlesAdd = ref<any[]>([])
 | 
			
		||||
const isActiveAddNews = ref(0)
 | 
			
		||||
const dialogImageVisible = ref(false)
 | 
			
		||||
const operateMaterial = ref('add')
 | 
			
		||||
const operateMaterial = ref<'add' | 'edit'>('add')
 | 
			
		||||
const articlesMediaId = ref('')
 | 
			
		||||
const hackResetEditor = ref(false)
 | 
			
		||||
 | 
			
		||||
/** 侦听公众号变化 **/
 | 
			
		||||
const accountChanged = (accountId) => {
 | 
			
		||||
  setAccountId(accountId)
 | 
			
		||||
const onAccountChanged = (id?: number) => {
 | 
			
		||||
  setAccountId(id)
 | 
			
		||||
  getList()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ======================== 列表查询 ========================
 | 
			
		||||
/** 设置账号编号 */
 | 
			
		||||
const setAccountId = (accountId) => {
 | 
			
		||||
  queryParams.accountId = accountId
 | 
			
		||||
  uploadData.accountId = accountId
 | 
			
		||||
const setAccountId = (id?: number) => {
 | 
			
		||||
  queryParams.accountId = id
 | 
			
		||||
  uploadData.accountId = id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 查询列表 */
 | 
			
		||||
const getList = async () => {
 | 
			
		||||
  // 如果没有选中公众号账号,则进行提示。
 | 
			
		||||
  if (!queryParams.accountId) {
 | 
			
		||||
    message.error('未选中公众号,无法查询草稿箱')
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    const drafts = await MpDraftApi.getDraftPage(queryParams)
 | 
			
		||||
@@ -329,7 +337,7 @@ const handleAdd = () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 更新按钮操作 */
 | 
			
		||||
const handleUpdate = (item) => {
 | 
			
		||||
const handleUpdate = (item: any) => {
 | 
			
		||||
  resetEditor()
 | 
			
		||||
  reset()
 | 
			
		||||
  articlesMediaId.value = item.mediaId
 | 
			
		||||
@@ -340,39 +348,30 @@ const handleUpdate = (item) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 提交按钮 */
 | 
			
		||||
const submitForm = () => {
 | 
			
		||||
  // TODO @Dhb52: 参考别的模块写法,改成 await 方式
 | 
			
		||||
const submitForm = async () => {
 | 
			
		||||
  addMaterialLoading.value = true
 | 
			
		||||
  if (operateMaterial.value === 'add') {
 | 
			
		||||
    MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value)
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        message.notifySuccess('新增成功')
 | 
			
		||||
        dialogNewsVisible.value = false
 | 
			
		||||
        getList()
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        addMaterialLoading.value = false
 | 
			
		||||
      })
 | 
			
		||||
  } else {
 | 
			
		||||
    MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value)
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        message.notifySuccess('更新成功')
 | 
			
		||||
        dialogNewsVisible.value = false
 | 
			
		||||
        getList()
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        addMaterialLoading.value = false
 | 
			
		||||
      })
 | 
			
		||||
  try {
 | 
			
		||||
    if (operateMaterial.value === 'add') {
 | 
			
		||||
      await MpDraftApi.createDraft(queryParams.accountId, articlesAdd.value)
 | 
			
		||||
      message.notifySuccess('新增成功')
 | 
			
		||||
    } else {
 | 
			
		||||
      await MpDraftApi.updateDraft(queryParams.accountId, articlesMediaId.value, articlesAdd.value)
 | 
			
		||||
      message.notifySuccess('更新成功')
 | 
			
		||||
    }
 | 
			
		||||
  } finally {
 | 
			
		||||
    dialogNewsVisible.value = false
 | 
			
		||||
    addMaterialLoading.value = false
 | 
			
		||||
    await getList()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 关闭弹窗
 | 
			
		||||
const dialogNewsClose = async (done) => {
 | 
			
		||||
const dialogNewsClose = async (onDone: () => {}) => {
 | 
			
		||||
  try {
 | 
			
		||||
    await message.confirm('修改内容可能还未保存,确定关闭吗?')
 | 
			
		||||
    reset()
 | 
			
		||||
    resetEditor()
 | 
			
		||||
    done()
 | 
			
		||||
    onDone()
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -391,7 +390,7 @@ const resetEditor = () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 将图文向下移动
 | 
			
		||||
const downNews = (index) => {
 | 
			
		||||
const downNews = (index: number) => {
 | 
			
		||||
  let temp = articlesAdd.value[index]
 | 
			
		||||
  articlesAdd.value[index] = articlesAdd.value[index + 1]
 | 
			
		||||
  articlesAdd.value[index + 1] = temp
 | 
			
		||||
@@ -407,13 +406,13 @@ const upNews = (index) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 选中指定 index 的图文
 | 
			
		||||
const activeNews = (index) => {
 | 
			
		||||
const activeNews = (index: number) => {
 | 
			
		||||
  resetEditor()
 | 
			
		||||
  isActiveAddNews.value = index
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除指定 index 的图文
 | 
			
		||||
const minusNews = async (index) => {
 | 
			
		||||
const minusNews = async (index: number) => {
 | 
			
		||||
  try {
 | 
			
		||||
    await message.confirm('确定删除该图文吗?')
 | 
			
		||||
    articlesAdd.value.splice(index, 1)
 | 
			
		||||
@@ -446,20 +445,17 @@ const buildEmptyArticle = () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ======================== 文件上传 ========================
 | 
			
		||||
const beforeThumbImageUpload = (file) => {
 | 
			
		||||
const beforeThumbImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
 | 
			
		||||
  addMaterialLoading.value = true
 | 
			
		||||
  const isType =
 | 
			
		||||
    file.type === 'image/jpeg' ||
 | 
			
		||||
    file.type === 'image/png' ||
 | 
			
		||||
    file.type === 'image/gif' ||
 | 
			
		||||
    file.type === 'image/bmp' ||
 | 
			
		||||
    file.type === 'image/jpg'
 | 
			
		||||
  const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes(
 | 
			
		||||
    rawFile.type
 | 
			
		||||
  )
 | 
			
		||||
  if (!isType) {
 | 
			
		||||
    message.error('上传图片格式不对!')
 | 
			
		||||
    addMaterialLoading.value = false
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  const isLt = file.size / 1024 / 1024 < 2
 | 
			
		||||
  const isLt = rawFile.size / 1024 / 1024 < 2
 | 
			
		||||
  if (!isLt) {
 | 
			
		||||
    message.error('上传图片大小不能超过 2M!')
 | 
			
		||||
    addMaterialLoading.value = false
 | 
			
		||||
@@ -469,7 +465,7 @@ const beforeThumbImageUpload = (file) => {
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleUploadSuccess = (response, file, fileList) => {
 | 
			
		||||
const handleUploadSuccess: UploadProps['onSuccess'] = (response: any) => {
 | 
			
		||||
  addMaterialLoading.value = false
 | 
			
		||||
  if (response.code !== 0) {
 | 
			
		||||
    message.error('上传出错:' + response.msg)
 | 
			
		||||
@@ -485,7 +481,7 @@ const handleUploadSuccess = (response, file, fileList) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 选择 or 上传完素材,设置回草稿
 | 
			
		||||
const selectMaterial = (item) => {
 | 
			
		||||
const selectMaterial = (item: any) => {
 | 
			
		||||
  dialogImageVisible.value = false
 | 
			
		||||
  articlesAdd.value[isActiveAddNews.value].thumbMediaId = item.mediaId
 | 
			
		||||
  articlesAdd.value[isActiveAddNews.value].thumbUrl = item.url
 | 
			
		||||
@@ -494,14 +490,10 @@ const selectMaterial = (item) => {
 | 
			
		||||
// 打开素材选择
 | 
			
		||||
const openMaterial = () => {
 | 
			
		||||
  dialogImageVisible.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    materialSelectRef.value.queryParams.accountId = queryParams.accountId // 强制设置下 accountId,避免二次查询不对
 | 
			
		||||
    materialSelectRef.value.handleQuery() // 刷新列表,失败也无所谓
 | 
			
		||||
  } catch (e) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ======================== 草稿箱发布 ========================
 | 
			
		||||
const handlePublish = async (item) => {
 | 
			
		||||
const handlePublish = async (item: any) => {
 | 
			
		||||
  const accountId = queryParams.accountId
 | 
			
		||||
  const mediaId = item.mediaId
 | 
			
		||||
  const content =
 | 
			
		||||
@@ -510,19 +502,19 @@ const handlePublish = async (item) => {
 | 
			
		||||
    await message.confirm(content)
 | 
			
		||||
    await MpFreePublishApi.submitFreePublish(accountId, mediaId)
 | 
			
		||||
    message.notifySuccess('发布成功')
 | 
			
		||||
    await getList()
 | 
			
		||||
    getList()
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 删除按钮操作 */
 | 
			
		||||
const handleDelete = async (item) => {
 | 
			
		||||
const handleDelete = async (item: any) => {
 | 
			
		||||
  const accountId = queryParams.accountId
 | 
			
		||||
  const mediaId = item.mediaId
 | 
			
		||||
  try {
 | 
			
		||||
    await message.confirm('此操作将永久删除该草稿, 是否继续?')
 | 
			
		||||
    await MpDraftApi.deleteDraft(accountId, mediaId)
 | 
			
		||||
    message.notifySuccess('删除成功')
 | 
			
		||||
    await getList()
 | 
			
		||||
    getList()
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
        <WxMpSelect @change="onAccountChanged" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <!-- 列表 -->
 | 
			
		||||
@@ -39,26 +48,32 @@
 | 
			
		||||
  </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'
 | 
			
		||||
import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
const { t } = useI18n() // 国际化
 | 
			
		||||
 | 
			
		||||
const loading = ref(true) // 列表的加载中
 | 
			
		||||
const total = ref(0) // 列表的总页数
 | 
			
		||||
const list = ref([]) // 列表的数据
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
const list = ref<any[]>([]) // 列表的数据
 | 
			
		||||
 | 
			
		||||
interface QueryParams {
 | 
			
		||||
  pageNo: number
 | 
			
		||||
  pageSize: number
 | 
			
		||||
  accountId?: number
 | 
			
		||||
}
 | 
			
		||||
const queryParams: QueryParams = reactive({
 | 
			
		||||
  pageNo: 1,
 | 
			
		||||
  pageSize: 10,
 | 
			
		||||
  accountId: undefined
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
/** 侦听公众号变化 **/
 | 
			
		||||
const accountChanged = (accountId) => {
 | 
			
		||||
  queryParams.accountId = accountId
 | 
			
		||||
const onAccountChanged = (id: number | undefined) => {
 | 
			
		||||
  queryParams.accountId = id
 | 
			
		||||
  getList()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -75,7 +90,7 @@ const getList = async () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 删除按钮操作 */
 | 
			
		||||
const handleDelete = async (item) => {
 | 
			
		||||
const handleDelete = async (item: any) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // 删除的二次确认
 | 
			
		||||
    await message.delConfirm('删除后用户将无法访问此页面,确定删除?')
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,13 @@
 | 
			
		||||
  <ContentWrap>
 | 
			
		||||
    <el-form class="-mb-15px" :inline="true" label-width="68px">
 | 
			
		||||
      <el-form-item label="公众号" prop="accountId">
 | 
			
		||||
        <WxMpSelect @change="accountChange" />
 | 
			
		||||
        <WxMpSelect @change="onAccountChanged" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <ContentWrap>
 | 
			
		||||
    <el-tabs v-model="type" @tab-change="handleTabChange">
 | 
			
		||||
    <el-tabs v-model="type" @tab-change="onTabChange">
 | 
			
		||||
      <!-- tab 1:图片  -->
 | 
			
		||||
      <el-tab-pane name="image">
 | 
			
		||||
        <template #label>
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -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('上传图片格式不对!')
 | 
			
		||||
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 = '视频'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!allowTypes.includes(rawFile.type)) {
 | 
			
		||||
    message.error(`上传${name}格式不对!`)
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  const isLt = rawFile.size / 1024 / 1024 < 2
 | 
			
		||||
  if (!isLt) {
 | 
			
		||||
    message.error('上传图片大小不能超过 2M!')
 | 
			
		||||
  // 校验大小
 | 
			
		||||
  if (rawFile.size / 1024 / 1024 > maxSizeMB) {
 | 
			
		||||
    message.error(`上传${name}大小不能超过${maxSizeMB}M!`)
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  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('上传语音格式不对!')
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  if (!isLt) {
 | 
			
		||||
    message.error('上传语音大小不能超过 2M!')
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
 | 
			
		||||
  beforeUpload(rawFile, 'image')
 | 
			
		||||
 | 
			
		||||
const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
 | 
			
		||||
  const isType = rawFile.type === 'video/mp4'
 | 
			
		||||
  if (!isType) {
 | 
			
		||||
    message.error('上传视频格式不对!')
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
 | 
			
		||||
  beforeUpload(rawFile, 'audio')
 | 
			
		||||
 | 
			
		||||
  const isLt = rawFile.size / 1024 / 1024 < 10
 | 
			
		||||
  if (!isLt) {
 | 
			
		||||
    message.error('上传视频大小不能超过 10M!')
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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">
 | 
			
		||||
        <WxMpSelect @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 WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 | 
			
		||||
import * as MpMenuApi from '@/api/mp/menu'
 | 
			
		||||
import { handleTree } from '@/utils/tree'
 | 
			
		||||
import menuOptions from './menuOptions'
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息
 | 
			
		||||
 | 
			
		||||
// ======================== 列表查询 ========================
 | 
			
		||||
const loading = ref(true) // 遮罩层
 | 
			
		||||
const accountId = ref(undefined) // 公众号Id
 | 
			
		||||
const 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
 | 
			
		||||
  // 提示,针对多图文
 | 
			
		||||
 
 | 
			
		||||
@@ -9,14 +9,7 @@
 | 
			
		||||
      label-width="68px"
 | 
			
		||||
    >
 | 
			
		||||
      <el-form-item label="公众号" prop="accountId">
 | 
			
		||||
        <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
 | 
			
		||||
          <el-option
 | 
			
		||||
            v-for="item in accountList"
 | 
			
		||||
            :key="item.id"
 | 
			
		||||
            :label="item.name"
 | 
			
		||||
            :value="item.id"
 | 
			
		||||
          />
 | 
			
		||||
        </el-select>
 | 
			
		||||
        <WxMpSelect @change="onAccountChanged" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="消息类型" prop="type">
 | 
			
		||||
        <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px">
 | 
			
		||||
@@ -84,70 +77,76 @@
 | 
			
		||||
      <el-table-column label="内容" prop="content">
 | 
			
		||||
        <template #default="scope">
 | 
			
		||||
          <!-- 【事件】区域 -->
 | 
			
		||||
          <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'">
 | 
			
		||||
          <div v-if="scope.row.type === MsgType.Event && scope.row.event === 'subscribe'">
 | 
			
		||||
            <el-tag type="success">关注</el-tag>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'unsubscribe'">
 | 
			
		||||
            <el-tag type="danger">取消关注</el-tag>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'CLICK'">
 | 
			
		||||
            <el-tag>点击菜单</el-tag>
 | 
			
		||||
            【{{ scope.row.eventKey }}】
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'VIEW'">
 | 
			
		||||
            <el-tag>点击菜单链接</el-tag>
 | 
			
		||||
            【{{ scope.row.eventKey }}】
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'">
 | 
			
		||||
          <div
 | 
			
		||||
            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_waitmsg'"
 | 
			
		||||
          >
 | 
			
		||||
            <el-tag>扫码结果</el-tag>
 | 
			
		||||
            【{{ scope.row.eventKey }}】
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_push'">
 | 
			
		||||
            <el-tag>扫码结果</el-tag>
 | 
			
		||||
            【{{ scope.row.eventKey }}】
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_sysphoto'">
 | 
			
		||||
            <el-tag>系统拍照发图</el-tag>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'">
 | 
			
		||||
          <div
 | 
			
		||||
            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_photo_or_album'"
 | 
			
		||||
          >
 | 
			
		||||
            <el-tag>拍照或者相册</el-tag>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_weixin'">
 | 
			
		||||
            <el-tag>微信相册</el-tag>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'">
 | 
			
		||||
          <div
 | 
			
		||||
            v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'location_select'"
 | 
			
		||||
          >
 | 
			
		||||
            <el-tag>选择地理位置</el-tag>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'event'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Event">
 | 
			
		||||
            <el-tag type="danger">未知事件类型</el-tag>
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- 【消息】区域 -->
 | 
			
		||||
          <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'voice'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Text">{{ scope.row.content }}</div>
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Voice">
 | 
			
		||||
            <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'image'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Image">
 | 
			
		||||
            <a target="_blank" :href="scope.row.mediaUrl">
 | 
			
		||||
              <img :src="scope.row.mediaUrl" style="width: 100px" />
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'video' || scope.row.type === 'shortvideo'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Video || scope.row.type === 'shortvideo'">
 | 
			
		||||
            <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'link'">
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Link">
 | 
			
		||||
            <el-tag>链接</el-tag>
 | 
			
		||||
            :
 | 
			
		||||
            <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'location'">
 | 
			
		||||
            <wx-location
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Location">
 | 
			
		||||
            <WxLocation
 | 
			
		||||
              :label="scope.row.label"
 | 
			
		||||
              :location-y="scope.row.locationY"
 | 
			
		||||
              :location-x="scope.row.locationX"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'music'">
 | 
			
		||||
            <wx-music
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.Music">
 | 
			
		||||
            <WxMusic
 | 
			
		||||
              :title="scope.row.title"
 | 
			
		||||
              :description="scope.row.description"
 | 
			
		||||
              :thumb-media-url="scope.row.thumbMediaUrl"
 | 
			
		||||
@@ -155,8 +154,8 @@
 | 
			
		||||
              :hq-music-url="scope.row.hqMusicUrl"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else-if="scope.row.type === 'news'">
 | 
			
		||||
            <wx-news :articles="scope.row.articles" />
 | 
			
		||||
          <div v-else-if="scope.row.type === MsgType.News">
 | 
			
		||||
            <WxNews :articles="scope.row.articles" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else>
 | 
			
		||||
            <el-tag type="danger">未知消息类型</el-tag>
 | 
			
		||||
@@ -177,7 +176,7 @@
 | 
			
		||||
      </el-table-column>
 | 
			
		||||
    </el-table>
 | 
			
		||||
    <!-- 分页组件 -->
 | 
			
		||||
    <pagination
 | 
			
		||||
    <Pagination
 | 
			
		||||
      v-show="total > 0"
 | 
			
		||||
      :total="total"
 | 
			
		||||
      v-model:page="queryParams.pageNo"
 | 
			
		||||
@@ -186,9 +185,14 @@
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <!-- 发送消息的弹窗 -->
 | 
			
		||||
    <el-dialog title="粉丝消息列表" v-model="open" @click="openDialog()" width="50%">
 | 
			
		||||
    <el-dialog
 | 
			
		||||
      title="粉丝消息列表"
 | 
			
		||||
      v-model="showMessageBox"
 | 
			
		||||
      @click="showMessageBox = true"
 | 
			
		||||
      width="50%"
 | 
			
		||||
    >
 | 
			
		||||
      <template #footer>
 | 
			
		||||
        <wx-msg :user-id="userId" v-if="open" />
 | 
			
		||||
        <WxMsg :user-id="userId" v-if="showMessageBox" />
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
@@ -200,17 +204,27 @@ import WxMsg from '@/views/mp/components/wx-msg/main.vue'
 | 
			
		||||
import WxLocation from '@/views/mp/components/wx-location/main.vue'
 | 
			
		||||
import WxMusic from '@/views/mp/components/wx-music/main.vue'
 | 
			
		||||
import WxNews from '@/views/mp/components/wx-news/main.vue'
 | 
			
		||||
import * as MpAccountApi from '@/api/mp/account'
 | 
			
		||||
import WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 | 
			
		||||
import * as MpMessageApi from '@/api/mp/message'
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 | 
			
		||||
import { dateFormatter } from '@/utils/formatTime'
 | 
			
		||||
import { MsgType } from '@/views/mp/components/wx-msg/types'
 | 
			
		||||
import type { FormInstance } from 'element-plus'
 | 
			
		||||
 | 
			
		||||
const loading = ref(true) // 列表的加载中
 | 
			
		||||
const total = ref(0) // 列表的总页数
 | 
			
		||||
const list = ref([]) // 列表的数据
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
const list = ref<any[]>([]) // 列表的数据
 | 
			
		||||
 | 
			
		||||
// 搜索参数
 | 
			
		||||
interface QueryParams {
 | 
			
		||||
  pageNo: number
 | 
			
		||||
  pageSize: number
 | 
			
		||||
  openid: string | null
 | 
			
		||||
  accountId: number | null
 | 
			
		||||
  type: MsgType | null
 | 
			
		||||
  createTime: string[] | []
 | 
			
		||||
}
 | 
			
		||||
const queryParams: QueryParams = reactive({
 | 
			
		||||
  pageNo: 1,
 | 
			
		||||
  pageSize: 10,
 | 
			
		||||
  openid: null,
 | 
			
		||||
@@ -218,19 +232,18 @@ const queryParams = reactive({
 | 
			
		||||
  type: null,
 | 
			
		||||
  createTime: []
 | 
			
		||||
})
 | 
			
		||||
const queryFormRef = ref() // 搜索的表单
 | 
			
		||||
// TODO 芋艿:下面应该移除
 | 
			
		||||
const open = ref(false) // 是否显示弹出层
 | 
			
		||||
const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
 | 
			
		||||
const showMessageBox = ref(false) // 是否显示弹出层
 | 
			
		||||
const userId = ref(0) // 操作的用户编号
 | 
			
		||||
const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
 | 
			
		||||
 | 
			
		||||
/** 侦听accountId */
 | 
			
		||||
const onAccountChanged = (id?: number) => {
 | 
			
		||||
  queryParams.accountId = id as number
 | 
			
		||||
  handleQuery()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 查询列表 */
 | 
			
		||||
const getList = async () => {
 | 
			
		||||
  // 如果没有选中公众号账号,则进行提示。
 | 
			
		||||
  if (!queryParams.accountId) {
 | 
			
		||||
    await message.error('未选中公众号,无法查询消息')
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  try {
 | 
			
		||||
    loading.value = true
 | 
			
		||||
    const data = await MpMessageApi.getMessagePage(queryParams)
 | 
			
		||||
@@ -249,34 +262,15 @@ const handleQuery = () => {
 | 
			
		||||
 | 
			
		||||
/** 重置按钮操作 */
 | 
			
		||||
const resetQuery = async () => {
 | 
			
		||||
  queryFormRef.value.resetFields()
 | 
			
		||||
  // 默认选中第一个
 | 
			
		||||
  if (accountList.value.length > 0) {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    queryParams.accountId = accountList.value[0].id
 | 
			
		||||
  }
 | 
			
		||||
  const accountId = queryParams.accountId
 | 
			
		||||
  queryFormRef.value?.resetFields()
 | 
			
		||||
  queryParams.accountId = accountId
 | 
			
		||||
  handleQuery()
 | 
			
		||||
}
 | 
			
		||||
const handleSend = async (row) => {
 | 
			
		||||
 | 
			
		||||
/** 打开消息发送窗口 */
 | 
			
		||||
const handleSend = async (row: any) => {
 | 
			
		||||
  userId.value = row.userId
 | 
			
		||||
  open.value = true
 | 
			
		||||
  showMessageBox.value = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const openDialog = () => {
 | 
			
		||||
  open.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>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" name="MpTagForm" setup>
 | 
			
		||||
import * as MpTagApi from '@/api/mp/tag'
 | 
			
		||||
import type { FormInstance, FormRules } from 'element-plus'
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n() // 国际化
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
@@ -26,18 +27,22 @@ 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
 | 
			
		||||
@@ -56,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">
 | 
			
		||||
        <WxMpSelect @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 WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
const { t } = useI18n() // 国际化
 | 
			
		||||
 | 
			
		||||
const loading = ref(true) // 列表的加载中
 | 
			
		||||
const total = ref(0) // 列表的总页数
 | 
			
		||||
const list = ref([]) // 列表的数据
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
const list = ref<any>([]) // 列表的数据
 | 
			
		||||
 | 
			
		||||
interface QueryParams {
 | 
			
		||||
  pageNo: number
 | 
			
		||||
  pageSize: number
 | 
			
		||||
  accountId?: number
 | 
			
		||||
}
 | 
			
		||||
const queryParams: QueryParams = reactive({
 | 
			
		||||
  pageNo: 1,
 | 
			
		||||
  pageSize: 10,
 | 
			
		||||
  accountId: undefined
 | 
			
		||||
})
 | 
			
		||||
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">
 | 
			
		||||
        <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']">
 | 
			
		||||
          <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 WxMpSelect from '@/views/mp/components/WxMpSelect.vue'
 | 
			
		||||
import type { FormInstance } from 'element-plus'
 | 
			
		||||
import UserForm from './UserForm.vue'
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息
 | 
			
		||||
 | 
			
		||||
const loading = ref(true) // 列表的加载中
 | 
			
		||||
const total = ref(0) // 列表的总页数
 | 
			
		||||
const list = ref([]) // 列表的数据
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
const list = ref<any[]>([]) // 列表的数据
 | 
			
		||||
 | 
			
		||||
interface QueryParams {
 | 
			
		||||
  pageNo: number
 | 
			
		||||
  pageSize: number
 | 
			
		||||
  accountId?: number
 | 
			
		||||
  openid: string | null
 | 
			
		||||
  nickname: string | null
 | 
			
		||||
}
 | 
			
		||||
const queryParams: QueryParams = reactive({
 | 
			
		||||
  pageNo: 1,
 | 
			
		||||
  pageSize: 10,
 | 
			
		||||
  accountId: null,
 | 
			
		||||
  accountId: undefined,
 | 
			
		||||
  openid: null,
 | 
			
		||||
  nickname: null
 | 
			
		||||
})
 | 
			
		||||
const 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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user