update V3.0.0

This commit is contained in:
hupeng committed 2024-06-02 10:52:34 +08:00
1 parent 3b4e0ca614
commit b16dd30f0b
1227 files changed
+337804 -4

No files matched your search

@@ -0,0 +1,171 @@
<template>
<div>
<!-- 情况一:已经选择好素材、或者上传好图片 -->
<div class="select-item" v-if="reply.url">
<img class="material-img" :src="reply.url" />
<p class="item-name" v-if="reply.name">{{ reply.name }}</p>
<el-row class="ope-row" justify="center">
<el-button type="danger" circle @click="onDelete">
<Icon icon="ep:delete" />
</el-button>
</el-row>
</div>
<!-- 情况二:未做完上述操作 -->
<el-row v-else style="text-align: center" align="middle">
<!-- 选择素材 -->
<el-col :span="12" class="col-select">
<el-button type="success" @click="showDialog = true">
素材库选择 <Icon icon="ep:circle-check" />
</el-button>
<el-dialog
title="选择图片"
v-model="showDialog"
width="90%"
append-to-body
destroy-on-close
>
<WxMaterialSelect
type="image"
:account-id="reply.accountId"
@select-material="selectMaterial"
/>
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12" class="col-add">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeImageUpload"
:on-success="onUploadSuccess"
>
<el-button type="primary">上传图片</el-button>
<template #tip>
<span>
<div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div>
</span>
</template>
</el-upload>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import type { UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth'
import { Reply } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
const props = defineProps<{
modelValue: Reply
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Reply)
}>()
const reply = computed<Reply>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const fileList = ref([])
const uploadData = reactive({
accountId: reply.value.accountId,
type: 'image',
title: '',
introduction: ''
})
const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
const onUploadSuccess = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
// 清空上传时的各种数据
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
// 上传好的文件,本质是个素材,所以可以进行选中
selectMaterial(res.data)
}
const onDelete = () => {
reply.value.mediaId = null
reply.value.url = null
reply.value.name = null
}
const selectMaterial = (item) => {
showDialog.value = false
// reply.value.type = 'image'
reply.value.mediaId = item.mediaId
reply.value.url = item.url
reply.value.name = item.name
}
</script>
<style lang="scss" scoped>
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #eaeaea;
.material-img {
width: 100%;
}
.item-name {
overflow: hidden;
font-size: 12px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
.item-infos {
width: 30%;
margin: auto;
}
.ope-row {
padding-top: 10px;
text-align: center;
}
}
.col-select {
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
}
.col-add {
float: right;
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
.el-upload__tip {
line-height: 18px;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,116 @@
<template>
<div>
<el-row align="middle" justify="center">
<el-col :span="6">
<el-row align="middle" justify="center" class="thumb-div">
<el-col :span="24">
<el-row align="middle" justify="center">
<img style="width: 100px" v-if="reply.thumbMediaUrl" :src="reply.thumbMediaUrl" />
<icon v-else icon="ep:plus" />
</el-row>
<el-row align="middle" justify="center" style="margin-top: 2%">
<div class="thumb-but">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeImageUpload"
:on-success="onUploadSuccess"
>
<template #trigger>
<el-button type="primary" link>本地上传</el-button>
</template>
<el-button type="primary" link @click="showDialog = true" style="margin-left: 5px"
>素材库选择
</el-button>
</el-upload>
</div>
</el-row>
</el-col>
</el-row>
<el-dialog
title="选择图片"
v-model="showDialog"
width="80%"
append-to-body
destroy-on-close
>
<WxMaterialSelect
type="image"
:account-id="reply.accountId"
@select-material="selectMaterial"
/>
</el-dialog>
</el-col>
<el-col :span="18">
<el-input v-model="reply.title" placeholder="请输入标题" />
<div style="margin: 20px 0"></div>
<el-input v-model="reply.description" placeholder="请输入描述" />
</el-col>
</el-row>
<div style="margin: 20px 0"></div>
<el-input v-model="reply.musicUrl" placeholder="请输入音乐链接" />
<div style="margin: 20px 0"></div>
<el-input v-model="reply.hqMusicUrl" placeholder="请输入高质量音乐链接" />
</div>
</template>
<script lang="ts" setup>
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import type { UploadRawFile } from 'element-plus'
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import { getAccessToken } from '@/utils/auth'
import { Reply } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
const props = defineProps<{
modelValue: Reply
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Reply)
}>()
const reply = computed<Reply>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const fileList = ref([])
const uploadData = reactive({
accountId: reply.value.accountId,
type: 'thumb', // 音乐类型为thumb
title: '',
introduction: ''
})
const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
const onUploadSuccess = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
// 清空上传时的各种数据
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
// 上传好的文件,本质是个素材,所以可以进行选中
selectMaterial(res.data)
}
const selectMaterial = (item: any) => {
showDialog.value = false
reply.value.thumbMediaId = item.mediaId
reply.value.thumbMediaUrl = item.url
}
</script>
@@ -0,0 +1,76 @@
<template>
<div>
<el-row>
<div class="select-item" v-if="reply.articles && reply.articles.length > 0">
<WxNews :articles="reply.articles" />
<el-col class="ope-row">
<el-button type="danger" circle @click="onDelete">
<Icon icon="ep:delete" />
</el-button>
</el-col>
</div>
<!-- 选择素材 -->
<el-col :span="24" v-if="!reply.content">
<el-row style="text-align: center" align="middle">
<el-col :span="24">
<el-button type="success" @click="showDialog = true">
{{ newsType === NewsType.Published ? '选择已发布图文' : '选择草稿箱图文' }}
<Icon icon="ep:circle-check" />
</el-button>
</el-col>
</el-row>
</el-col>
<el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close>
<WxMaterialSelect
type="news"
:account-id="reply.accountId"
:newsType="newsType"
@select-material="selectMaterial"
/>
</el-dialog>
</el-row>
</div>
</template>
<script lang="ts" setup>
import WxNews from '@/views/mp/components/wx-news'
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import { Reply, NewsType } from './types'
const props = defineProps<{
modelValue: Reply
newsType: NewsType
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Reply)
}>()
const reply = computed<Reply>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const selectMaterial = (item: any) => {
showDialog.value = false
reply.value.articles = item.content.newsItem
}
const onDelete = () => {
reply.value.articles = []
}
</script>
<style lang="scss" scoped>
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #eaeaea;
.ope-row {
padding-top: 10px;
text-align: center;
}
}
</style>
@@ -0,0 +1,22 @@
<template>
<el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="content" />
</template>
<script lang="ts" setup>
const props = defineProps<{
modelValue?: string | null
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: string | null)
(e: 'input', v: string | null)
}>()
const content = computed<string | null | undefined>({
get: () => props.modelValue,
set: (val: string | null) => {
emit('update:modelValue', val)
emit('input', val)
}
})
</script>
@@ -0,0 +1,128 @@
<template>
<div>
<el-row>
<el-input v-model="reply.title" class="input-margin-bottom" placeholder="请输入标题" />
<el-input class="input-margin-bottom" v-model="reply.description" placeholder="请输入描述" />
<el-row class="ope-row" justify="center">
<WxVideoPlayer v-if="reply.url" :url="reply.url" />
</el-row>
<el-col>
<el-row style="text-align: center" align="middle">
<!-- 选择素材 -->
<el-col :span="12">
<el-button type="success" @click="showDialog = true">
素材库选择 <Icon icon="ep:circle-check" />
</el-button>
<el-dialog
title="选择视频"
v-model="showDialog"
width="90%"
append-to-body
destroy-on-close
>
<WxMaterialSelect
type="video"
:account-id="reply.accountId"
@select-material="selectMaterial"
/>
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVideoUpload"
:on-success="onUploadSuccess"
>
<el-button type="primary">新建视频 <Icon icon="ep:upload" /></el-button>
</el-upload>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import type { UploadRawFile } from 'element-plus'
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import { getAccessToken } from '@/utils/auth'
import { Reply } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() }
const props = defineProps<{
modelValue: Reply
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Reply)
}>()
const reply = computed<Reply>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const fileList = ref([])
const uploadData = reactive({
accountId: reply.value.accountId,
type: 'video',
title: '',
introduction: ''
})
const beforeVideoUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Video, 10)(rawFile)
const onUploadSuccess = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
// 清空上传时的各种数据
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
selectMaterial(res.data)
}
/** 选择素材后设置 */
const selectMaterial = (item: any) => {
showDialog.value = false
reply.value.mediaId = item.mediaId
reply.value.url = item.url
reply.value.name = item.name
// title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction
if (item.title) {
reply.value.title = item.title || ''
}
if (item.introduction) {
reply.value.description = item.introduction || ''
}
}
</script>
<style lang="scss" scoped>
.input-margin-bottom {
margin-bottom: 2%;
}
.ope-row {
width: 100%;
padding-top: 10px;
text-align: center;
}
</style>
@@ -0,0 +1,160 @@
<template>
<div>
<div class="select-item2" v-if="reply.url">
<p class="item-name">{{ reply.name }}</p>
<el-row class="ope-row" justify="center">
<WxVoicePlayer :url="reply.url" />
</el-row>
<el-row class="ope-row" justify="center">
<el-button type="danger" circle @click="onDelete"><Icon icon="ep:delete" /></el-button>
</el-row>
</div>
<el-row v-else style="text-align: center">
<!-- 选择素材 -->
<el-col :span="12" class="col-select">
<el-button type="success" @click="showDialog = true">
素材库选择<Icon icon="ep:circle-check" />
</el-button>
<el-dialog
title="选择语音"
v-model="showDialog"
width="90%"
append-to-body
destroy-on-close
>
<WxMaterialSelect
type="voice"
:account-id="reply.accountId"
@select-material="selectMaterial"
/>
</el-dialog>
</el-col>
<!-- 文件上传 -->
<el-col :span="12" class="col-add">
<el-upload
:action="UPLOAD_URL"
:headers="HEADERS"
multiple
:limit="1"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVoiceUpload"
:on-success="onUploadSuccess"
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">
格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s
</div>
</template>
</el-upload>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import type { UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth'
import { Reply } from './types'
const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
const props = defineProps<{
modelValue: Reply
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Reply)
}>()
const reply = computed<Reply>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const showDialog = ref(false)
const fileList = ref([])
const uploadData = reactive({
accountId: reply.value.accountId,
type: 'voice',
title: '',
introduction: ''
})
const beforeVoiceUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Voice, 10)(rawFile)
const onUploadSuccess = (res: any) => {
if (res.code !== 0) {
message.error('上传出错:' + res.msg)
return false
}
// 清空上传时的各种数据
fileList.value = []
uploadData.title = ''
uploadData.introduction = ''
// 上传好的文件,本质是个素材,所以可以进行选中
selectMaterial(res.data)
}
const onDelete = () => {
reply.value.mediaId = null
reply.value.url = null
reply.value.name = null
}
const selectMaterial = (item: Reply) => {
showDialog.value = false
// reply.value.type = ReplyType.Voice
reply.value.mediaId = item.mediaId
reply.value.url = item.url
reply.value.name = item.name
}
</script>
<style lang="scss" scoped>
.select-item2 {
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #eaeaea;
.item-name {
overflow: hidden;
font-size: 12px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
.ope-row {
width: 100%;
padding-top: 10px;
text-align: center;
}
}
.col-select {
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
}
.col-add {
float: right;
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
.el-upload__tip {
line-height: 18px;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,54 @@
enum ReplyType {
News = 'news',
Image = 'image',
Voice = 'voice',
Video = 'video',
Music = 'music',
Text = 'text'
}
interface _Reply {
accountId: number
type: ReplyType
name?: string | null
content?: string | null
mediaId?: string | null
url?: string | null
title?: string | null
description?: string | null
thumbMediaId?: string | null
thumbMediaUrl?: string | null
musicUrl?: string | null
hqMusicUrl?: string | null
introduction?: string | null
articles?: any[]
}
type Reply = _Reply //Partial<_Reply>
enum NewsType {
Published = '1',
Draft = '2'
}
/** 利用旧的reply[accountId, type]初始化新的Reply */
const createEmptyReply = (old: Reply | Ref<Reply>): Reply => {
return {
accountId: unref(old).accountId,
type: unref(old).type,
name: null,
content: null,
mediaId: null,
url: null,
title: null,
description: null,
thumbMediaId: null,
thumbMediaUrl: null,
musicUrl: null,
hqMusicUrl: null,
introduction: null,
articles: []
}
}
export { Reply, NewsType, ReplyType, createEmptyReply }
@@ -0,0 +1,7 @@
import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types'
import WxReplySelect from './main.vue'
export type { Reply }
export { createEmptyReply, NewsType, ReplyType }
export default WxReplySelect
@@ -0,0 +1,208 @@
<!--
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
芋道源码:
① 移除多余的 rep 为前缀的变量,让 message 消息更简单
② 代码优化,补充注释,提升阅读性
③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入
④ 支持发送【视频】消息时,支持新建视频
-->
<template>
<el-tabs type="border-card" v-model="currentTab">
<!-- 类型 1:文本 -->
<el-tab-pane :name="ReplyType.Text">
<template #label>
<el-row align="middle"><Icon icon="ep:document" /> 文本</el-row>
</template>
<TabText v-model="reply.content" />
</el-tab-pane>
<!-- 类型 2:图片 -->
<el-tab-pane :name="ReplyType.Image">
<template #label>
<el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 图片</el-row>
</template>
<TabImage v-model="reply" />
</el-tab-pane>
<!-- 类型 3:语音 -->
<el-tab-pane :name="ReplyType.Voice">
<template #label>
<el-row align="middle"><Icon icon="ep:phone" /> 语音</el-row>
</template>
<TabVoice v-model="reply" />
</el-tab-pane>
<!-- 类型 4:视频 -->
<el-tab-pane :name="ReplyType.Video">
<template #label>
<el-row align="middle"><Icon icon="ep:share" /> 视频</el-row>
</template>
<TabVideo v-model="reply" />
</el-tab-pane>
<!-- 类型 5:图文 -->
<el-tab-pane :name="ReplyType.News">
<template #label>
<el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row>
</template>
<TabNews v-model="reply" :news-type="newsType" />
</el-tab-pane>
<!-- 类型 6:音乐 -->
<el-tab-pane :name="ReplyType.Music">
<template #label>
<el-row align="middle"><Icon icon="ep:service" />音乐</el-row>
</template>
<TabMusic v-model="reply" />
</el-tab-pane>
</el-tabs>
</template>
<script lang="ts" setup>
import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types'
import TabText from './components/TabText.vue'
import TabImage from './components/TabImage.vue'
import TabVoice from './components/TabVoice.vue'
import TabVideo from './components/TabVideo.vue'
import TabNews from './components/TabNews.vue'
import TabMusic from './components/TabMusic.vue'
defineOptions({ name: 'WxReplySelect' })
interface Props {
modelValue: Reply
newsType?: NewsType
}
const props = withDefaults(defineProps<Props>(), {
newsType: () => NewsType.Published
})
const emit = defineEmits<{
(e: 'update:modelValue', v: Reply)
}>()
const reply = computed<Reply>({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
// 作为多个标签保存各自Reply的缓存
const tabCache = new Map<ReplyType, Reply>()
// 采用独立的ref来保存当前tab,避免在watch标签变化,对reply进行赋值会产生了循环调用
const currentTab = ref<ReplyType>(props.modelValue.type || ReplyType.Text)
watch(
currentTab,
(newTab, oldTab) => {
// 第一次进入:oldTab 为 undefined
// 判断 newTab 是因为 Reply 为 Partial
if (oldTab === undefined || newTab === undefined) {
return
}
tabCache.set(oldTab, unref(reply))
// 从缓存里面取出新tab内容,有则覆盖Reply,没有则创建空Reply
const temp = tabCache.get(newTab)
if (temp) {
reply.value = temp
} else {
let newData = createEmptyReply(reply)
newData.type = newTab
reply.value = newData
}
},
{
immediate: true
}
)
/** 清除除了`type`, `accountId`的字段 */
const clear = () => {
reply.value = createEmptyReply(reply)
}
defineExpose({
clear
})
</script>
<style lang="scss" scoped>
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #eaeaea;
}
.select-item2 {
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #eaeaea;
}
.ope-row {
padding-top: 10px;
text-align: center;
}
.input-margin-bottom {
margin-bottom: 2%;
}
.item-name {
overflow: hidden;
font-size: 12px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.el-form-item__content {
line-height: unset !important;
}
.col-select {
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
}
.col-select2 {
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
}
.col-add {
float: right;
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
}
.avatar-uploader-icon {
width: 100px !important;
height: 100px !important;
font-size: 28px;
line-height: 100px !important;
color: #8c939d;
text-align: center;
border: 1px solid #d9d9d9;
}
.material-img {
width: 100%;
}
.thumb-div {
display: inline-block;
text-align: center;
}
.item-infos {
width: 30%;
margin: auto;
}
</style>