!135 MP模块重构,功能增强

Merge pull request !135 from dhb52/dev
This commit is contained in:
芋道源码 2023-04-22 12:51:56 +00:00 committed by Gitee
commit 4cf734d244
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
49 changed files with 840 additions and 668 deletions

View File

@ -46,7 +46,7 @@
v-if="scope.row.qrCodeUrl" v-if="scope.row.qrCodeUrl"
:src="scope.row.qrCodeUrl" :src="scope.row.qrCodeUrl"
alt="二维码" alt="二维码"
style="height: 100px; display: inline-block" style="display: inline-block; height: 100px"
/> />
<el-button <el-button
link link

View File

@ -94,10 +94,10 @@
</el-table> </el-table>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVideoPlayer from '@/views/mp/components/wx-video-play'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxMusic from '@/views/mp/components/wx-music'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import { MsgType } from './types' import { MsgType } from './types'

View File

@ -5,44 +5,3 @@ export enum MsgType {
Message = 2, Message = 2,
Keyword = 3 Keyword = 3
} }
type ReplyType = 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link'
export 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
}
// TODO @Dhb52ObjData 这个类名可以在看看ObjData 有点通用
export 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
}

View File

@ -82,7 +82,7 @@
<el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable /> <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
</el-form-item> </el-form-item>
<el-form-item label="回复消息"> <el-form-item label="回复消息">
<WxReplySelect :objData="objData" /> <WxReplySelect v-model="reply" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@ -93,14 +93,14 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts" name="MpAutoReply"> <script setup lang="ts" name="MpAutoReply">
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxReplySelect, { type Reply, ReplyType } from '@/views/mp/components/wx-reply'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxAccountSelect from '@/views/mp/components/wx-account-select'
import * as MpAutoReplyApi from '@/api/mp/autoReply' import * as MpAutoReplyApi from '@/api/mp/autoReply'
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
import { ContentWrap } from '@/components/ContentWrap' import { ContentWrap } from '@/components/ContentWrap'
import type { TabPaneName } from 'element-plus' import type { FormInstance, TabPaneName } from 'element-plus'
import ReplyTable from './components/ReplyTable.vue' import ReplyTable from './components/ReplyTable.vue'
import { MsgType, ReplyForm, ObjData } from './components/types' import { MsgType } from './components/types'
const message = useMessage() // const message = useMessage() //
const msgType = ref<MsgType>(MsgType.Keyword) // const msgType = ref<MsgType>(MsgType.Keyword) //
@ -108,26 +108,26 @@ const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'l
const loading = ref(true) // const loading = ref(true) //
const total = ref(0) // const total = ref(0) //
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
const formRef = ref() // ref const formRef = ref<FormInstance | null>(null) // ref
// //
interface QueryParams { interface QueryParams {
pageNo: number pageNo: number
pageSize: number pageSize: number
accountId?: number accountId: number
} }
const queryParams: QueryParams = reactive({ const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: 0
}) })
const dialogTitle = ref('') // const dialogTitle = ref('') //
const showFormDialog = ref(false) // const showFormDialog = ref(false) //
const replyForm = ref<ReplyForm>({}) // const replyForm = ref<any>({}) //
// //
const objData = ref<ObjData>({ const reply = ref<Reply>({
type: 'text', type: ReplyType.Text,
accountId: undefined accountId: 0
}) })
// //
const rules = { const rules = {
@ -136,8 +136,9 @@ const rules = {
} }
/** 侦听账号变化 */ /** 侦听账号变化 */
const onAccountChanged = (id?: number) => { const onAccountChanged = (id: number) => {
queryParams.accountId = id queryParams.accountId = id
reply.value.accountId = id
getList() getList()
} }
@ -171,8 +172,8 @@ const onTabChange = (tabName: TabPaneName) => {
const onCreate = () => { const onCreate = () => {
reset() reset()
// //
objData.value = { reply.value = {
type: 'text', type: ReplyType.Text,
accountId: queryParams.accountId accountId: queryParams.accountId
} }
@ -193,7 +194,7 @@ const onUpdate = async (id: number) => {
delete replyForm.value['responseMediaUrl'] delete replyForm.value['responseMediaUrl']
delete replyForm.value['responseDescription'] delete replyForm.value['responseDescription']
delete replyForm.value['responseArticles'] delete replyForm.value['responseArticles']
objData.value = { reply.value = {
type: data.responseMessageType, type: data.responseMessageType,
accountId: queryParams.accountId, accountId: queryParams.accountId,
content: data.responseContent, content: data.responseContent,
@ -227,17 +228,17 @@ const onSubmit = async () => {
// //
const submitForm: any = { ...replyForm.value } const submitForm: any = { ...replyForm.value }
submitForm.responseMessageType = objData.value.type submitForm.responseMessageType = reply.value.type
submitForm.responseContent = objData.value.content submitForm.responseContent = reply.value.content
submitForm.responseMediaId = objData.value.mediaId submitForm.responseMediaId = reply.value.mediaId
submitForm.responseMediaUrl = objData.value.url submitForm.responseMediaUrl = reply.value.url
submitForm.responseTitle = objData.value.title submitForm.responseTitle = reply.value.title
submitForm.responseDescription = objData.value.description submitForm.responseDescription = reply.value.description
submitForm.responseThumbMediaId = objData.value.thumbMediaId submitForm.responseThumbMediaId = reply.value.thumbMediaId
submitForm.responseThumbMediaUrl = objData.value.thumbMediaUrl submitForm.responseThumbMediaUrl = reply.value.thumbMediaUrl
submitForm.responseArticles = objData.value.articles submitForm.responseArticles = reply.value.articles
submitForm.responseMusicUrl = objData.value.musicUrl submitForm.responseMusicUrl = reply.value.musicUrl
submitForm.responseHqMusicUrl = objData.value.hqMusicUrl submitForm.responseHqMusicUrl = reply.value.hqMusicUrl
if (replyForm.value.id !== undefined) { if (replyForm.value.id !== undefined) {
await MpAutoReplyApi.updateAutoReply(submitForm) await MpAutoReplyApi.updateAutoReply(submitForm)

View File

@ -0,0 +1,3 @@
import WxAccountSelect from './main.vue'
export default WxAccountSelect

View File

@ -14,7 +14,7 @@ const account: MpAccountApi.AccountVO = reactive({
const accountList: Ref<MpAccountApi.AccountVO[]> = ref([]) const accountList: Ref<MpAccountApi.AccountVO[]> = ref([])
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', id?: number, name?: string): void (e: 'change', id: number, name: string): void
}>() }>()
const handleQuery = async () => { const handleQuery = async () => {
@ -22,15 +22,19 @@ const handleQuery = async () => {
// //
if (accountList.value.length > 0) { if (accountList.value.length > 0) {
account.id = accountList.value[0].id account.id = accountList.value[0].id
account.name = accountList.value[0].name if (account.id) {
emit('change', account.id, account.name) account.name = accountList.value[0].name
emit('change', account.id, account.name)
}
} }
} }
const onChanged = (id?: number) => { const onChanged = (id?: number) => {
const found = accountList.value.find((v) => v.id === id) const found = accountList.value.find((v) => v.id === id)
account.name = found ? found.name : '' if (account.id) {
emit('change', account.id, account.name) account.name = found ? found.name : ''
emit('change', account.id, account.name)
}
} }
/** 初始化 */ /** 初始化 */

View File

@ -0,0 +1,3 @@
import WxLocation from './main.vue'
export default WxLocation

View File

@ -0,0 +1,6 @@
import WxMaterialSelect from './main.vue'
import { NewsType, MaterialType } from './types'
export { NewsType, MaterialType }
export default WxMaterialSelect

View File

@ -7,7 +7,7 @@
<template> <template>
<div class="pb-30px"> <div class="pb-30px">
<!-- 类型image --> <!-- 类型image -->
<div v-if="objData.type === 'image'"> <div v-if="props.type === 'image'">
<div class="waterfall" v-loading="loading"> <div class="waterfall" v-loading="loading">
<div class="waterfall-item" v-for="item in list" :key="item.mediaId"> <div class="waterfall-item" v-for="item in list" :key="item.mediaId">
<img class="material-img" :src="item.url" /> <img class="material-img" :src="item.url" />
@ -29,7 +29,7 @@
/> />
</div> </div>
<!-- 类型voice --> <!-- 类型voice -->
<div v-else-if="objData.type === 'voice'"> <div v-else-if="props.type === 'voice'">
<!-- 列表 --> <!-- 列表 -->
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="mediaId" /> <el-table-column label="编号" align="center" prop="mediaId" />
@ -64,7 +64,7 @@
/> />
</div> </div>
<!-- 类型video --> <!-- 类型video -->
<div v-else-if="objData.type === 'video'"> <div v-else-if="props.type === 'video'">
<!-- 列表 --> <!-- 列表 -->
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="mediaId" /> <el-table-column label="编号" align="center" prop="mediaId" />
@ -106,7 +106,7 @@
/> />
</div> </div>
<!-- 类型news --> <!-- 类型news -->
<div v-else-if="objData.type === 'news'"> <div v-else-if="props.type === 'news'">
<div class="waterfall" v-loading="loading"> <div class="waterfall" v-loading="loading">
<div class="waterfall-item" v-for="item in list" :key="item.mediaId"> <div class="waterfall-item" v-for="item in list" :key="item.mediaId">
<div v-if="item.content && item.content.newsItem"> <div v-if="item.content && item.content.newsItem">
@ -132,25 +132,25 @@
</template> </template>
<script lang="ts" setup name="WxMaterialSelect"> <script lang="ts" setup name="WxMaterialSelect">
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVideoPlayer from '@/views/mp/components/wx-video-play'
import { NewsType } from './types'
import * as MpMaterialApi from '@/api/mp/material' import * as MpMaterialApi from '@/api/mp/material'
import * as MpFreePublishApi from '@/api/mp/freePublish' import * as MpFreePublishApi from '@/api/mp/freePublish'
import * as MpDraftApi from '@/api/mp/draft' import * as MpDraftApi from '@/api/mp/draft'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
const props = defineProps({ const props = withDefaults(
objData: { defineProps<{
type: Object, // type - accountId - type: string
required: true accountId: number
}, newsType?: NewsType
newsType: { }>(),
// 12稿 {
type: String as PropType<string>, newsType: NewsType.Published
default: '1'
} }
}) )
const emit = defineEmits(['select-material']) const emit = defineEmits(['select-material'])
@ -159,15 +159,13 @@ const loading = ref(false)
// //
const total = ref(0) const total = ref(0)
// //
const list = ref([]) const list = ref<any[]>([])
// //
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: props.objData.accountId accountId: props.accountId
}) })
const objDataRef = reactive(props.objData)
const newsTypeRef = ref(props.newsType)
const selectMaterialFun = (item) => { const selectMaterialFun = (item) => {
emit('select-material', item) emit('select-material', item)
@ -176,10 +174,10 @@ const selectMaterialFun = (item) => {
const getPage = async () => { const getPage = async () => {
loading.value = true loading.value = true
try { try {
if (objDataRef.type === 'news' && newsTypeRef.value === '1') { if (props.type === 'news' && props.newsType === NewsType.Published) {
// + // +
await getFreePublishPageFun() await getFreePublishPageFun()
} else if (objDataRef.type === 'news' && newsTypeRef.value === '2') { } else if (props.type === 'news' && props.newsType === NewsType.Draft) {
// + 稿 // + 稿
await getDraftPageFun() await getDraftPageFun()
} else { } else {
@ -194,7 +192,7 @@ const getPage = async () => {
const getMaterialPageFun = async () => { const getMaterialPageFun = async () => {
const data = await MpMaterialApi.getMaterialPage({ const data = await MpMaterialApi.getMaterialPage({
...queryParams, ...queryParams,
type: objDataRef.type type: props.type
}) })
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
@ -202,9 +200,9 @@ const getMaterialPageFun = async () => {
const getFreePublishPageFun = async () => { const getFreePublishPageFun = async () => {
const data = await MpFreePublishApi.getFreePublishPage(queryParams) const data = await MpFreePublishApi.getFreePublishPage(queryParams)
data.list.forEach((item) => { data.list.forEach((item: any) => {
const newsItem = item.content.newsItem const articles = item.content.newsItem
newsItem.forEach((article) => { articles.forEach((article: any) => {
article.picUrl = article.thumbUrl article.picUrl = article.thumbUrl
}) })
}) })
@ -214,9 +212,9 @@ const getFreePublishPageFun = async () => {
const getDraftPageFun = async () => { const getDraftPageFun = async () => {
const data = await MpDraftApi.getDraftPage(queryParams) const data = await MpDraftApi.getDraftPage(queryParams)
data.list.forEach((item) => { data.list.forEach((draft: any) => {
const newsItem = item.content.newsItem const articles = draft.content.newsItem
newsItem.forEach((article) => { articles.forEach((article: any) => {
article.picUrl = article.thumbUrl article.picUrl = article.thumbUrl
}) })
}) })
@ -229,29 +227,6 @@ onMounted(async () => {
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin: 0 auto;
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
.material-img {
width: 100%;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) { @media (min-width: 992px) and (max-width: 1300px) {
.waterfall { .waterfall {
column-count: 3; column-count: 3;
@ -278,5 +253,25 @@ p {
} }
} }
/*瀑布流样式*/ .waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin: 0 auto;
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
.material-img {
width: 100%;
}
p {
line-height: 30px;
}
</style> </style>

View File

@ -0,0 +1,11 @@
export enum NewsType {
Draft = '2',
Published = '1'
}
export enum MaterialType {
Image = 'image',
Voice = 'voice',
Video = 'video',
News = 'news'
}

View File

@ -1,25 +1,27 @@
.avue-card{ .avue-card {
&__item{ &__item {
margin-bottom: 16px; margin-bottom: 16px;
border: 1px solid #e8e8e8; border: 1px solid #e8e8e8;
background-color: #fff; background-color: #fff;
box-sizing: border-box; box-sizing: border-box;
color: rgba(0,0,0,.65); color: rgba(0, 0, 0, 0.65);
font-size: 14px; font-size: 14px;
font-variant: tabular-nums; font-variant: tabular-nums;
line-height: 1.5; line-height: 1.5;
list-style: none; list-style: none;
font-feature-settings: "tnum"; font-feature-settings: 'tnum';
cursor: pointer; cursor: pointer;
height:200px; height: 200px;
&:hover{
border-color: rgba(0,0,0,.09); &:hover {
box-shadow: 0 2px 8px rgba(0,0,0,.09); border-color: rgba(0, 0, 0, 0.09);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
} }
&--add{
border:1px dashed #000; &--add {
border: 1px dashed #000;
width: 100%; width: 100%;
color: rgba(0,0,0,.45); color: rgba(0, 0, 0, 0.45);
background-color: #fff; background-color: #fff;
border-color: #d9d9d9; border-color: #d9d9d9;
border-radius: 2px; border-radius: 2px;
@ -27,74 +29,87 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px; font-size: 16px;
i{
i {
margin-right: 10px; margin-right: 10px;
} }
&:hover{
&:hover {
color: #40a9ff; color: #40a9ff;
background-color: #fff; background-color: #fff;
border-color: #40a9ff; border-color: #40a9ff;
} }
} }
} }
&__body{
&__body {
display: flex; display: flex;
padding: 24px; padding: 24px;
} }
&__detail{
flex:1 &__detail {
flex: 1;
} }
&__avatar{
&__avatar {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 48px; border-radius: 48px;
overflow: hidden; overflow: hidden;
margin-right: 12px; margin-right: 12px;
img{
img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
&__title{
color: rgba(0,0,0,.85); &__title {
color: rgba(0, 0, 0, 0.85);
margin-bottom: 12px; margin-bottom: 12px;
font-size: 16px; font-size: 16px;
&:hover{
color:#1890ff; &:hover {
color: #1890ff;
} }
} }
&__info{
color: rgba(0,0,0,.45); &__info {
color: rgba(0, 0, 0, 0.45);
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
overflow: hidden; overflow: hidden;
height: 64px; height: 64px;
} }
&__menu{
&__menu {
display: flex; display: flex;
justify-content:space-around; justify-content: space-around;
height: 50px; height: 50px;
background: #f7f9fa; background: #f7f9fa;
color: rgba(0,0,0,.45); color: rgba(0, 0, 0, 0.45);
text-align: center; text-align: center;
line-height: 50px; line-height: 50px;
&:hover{
color:#1890ff; &:hover {
color: #1890ff;
} }
} }
} }
/** joolun 额外加的 */ /** joolun 额外加的 */
.avue-comment__main { .avue-comment__main {
flex: unset!important; flex: unset !important;
border-radius: 5px!important; border-radius: 5px !important;
margin: 0 8px!important; margin: 0 8px !important;
} }
.avue-comment__header { .avue-comment__header {
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
} }
.avue-comment__body { .avue-comment__body {
border-bottom-right-radius: 5px; border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px; border-bottom-left-radius: 5px;

View File

@ -1,27 +1,33 @@
/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */ /* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */
.avue-comment{ .avue-comment {
margin-bottom: 30px; margin-bottom: 30px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
&--reverse{
flex-direction:row-reverse; &--reverse {
.avue-comment__main{ flex-direction: row-reverse;
&:before,&:after{
.avue-comment__main {
&:before,
&:after {
left: auto; left: auto;
right: -8px; right: -8px;
border-width: 8px 0 8px 8px; border-width: 8px 0 8px 8px;
} }
&:before{
&:before {
border-left-color: #dedede; border-left-color: #dedede;
} }
&:after{
&:after {
border-left-color: #f8f8f8; border-left-color: #f8f8f8;
margin-right: 1px; margin-right: 1px;
margin-left: auto; margin-left: auto;
} }
} }
} }
&__avatar{
&__avatar {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 50%; border-radius: 50%;
@ -29,7 +35,8 @@
box-sizing: border-box; box-sizing: border-box;
vertical-align: middle; vertical-align: middle;
} }
&__header{
&__header {
padding: 5px 15px; padding: 5px 15px;
background: #f8f8f8; background: #f8f8f8;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
@ -37,18 +44,22 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
&__author{
&__author {
font-weight: 700; font-weight: 700;
font-size: 14px; font-size: 14px;
color: #999; color: #999;
} }
&__main{
flex:1; &__main {
flex: 1;
margin: 0 20px; margin: 0 20px;
position: relative; position: relative;
border: 1px solid #dedede; border: 1px solid #dedede;
border-radius: 2px; border-radius: 2px;
&:before,&:after{
&:before,
&:after {
position: absolute; position: absolute;
top: 10px; top: 10px;
left: -8px; left: -8px;
@ -56,32 +67,39 @@
width: 0; width: 0;
height: 0; height: 0;
display: block; display: block;
content: " "; content: ' ';
border-color: transparent; border-color: transparent;
border-style: solid solid outset; border-style: solid solid outset;
border-width: 8px 8px 8px 0; border-width: 8px 8px 8px 0;
pointer-events: none; pointer-events: none;
} }
&:before { &:before {
border-right-color: #dedede; border-right-color: #dedede;
z-index: 1; z-index: 1;
} }
&:after{
&:after {
border-right-color: #f8f8f8; border-right-color: #f8f8f8;
margin-left: 1px; margin-left: 1px;
z-index: 2; z-index: 2;
} }
} }
&__body{
&__body {
padding: 15px; padding: 15px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
font-family: Segoe UI,Lucida Grande,Helvetica,Arial,Microsoft YaHei,FreeSans,Arimo,Droid Sans,wenquanyi micro hei,Hiragino Sans GB,Hiragino Sans GB W3,FontAwesome,sans-serif;color: #333; font-family: Segoe UI, Lucida Grande, Helvetica, Arial, Microsoft YaHei, FreeSans, Arimo,
Droid Sans, wenquanyi micro hei, Hiragino Sans GB, Hiragino Sans GB W3, FontAwesome,
sans-serif;
color: #333;
font-size: 14px; font-size: 14px;
} }
blockquote{
margin:0; blockquote {
font-family: Georgia,Times New Roman,Times,Kai,Kaiti SC,KaiTi,BiauKai,FontAwesome,serif; margin: 0;
font-family: Georgia, Times New Roman, Times, Kai, Kaiti SC, KaiTi, BiauKai, FontAwesome, serif;
padding: 1px 0 1px 15px; padding: 1px 0 1px 15px;
border-left: 4px solid #ddd; border-left: 4px solid #ddd;
} }

View File

@ -0,0 +1,6 @@
import WxMsg from './main.vue'
import { MsgType } from './types'
export { MsgType }
export default WxMsg

View File

@ -125,19 +125,19 @@
</div> </div>
</div> </div>
<div class="msg-send" v-loading="sendLoading"> <div class="msg-send" v-loading="sendLoading">
<WxReplySelect ref="replySelectRef" :objData="objData" /> <WxReplySelect ref="replySelectRef" v-model="reply" />
<el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button> <el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button>
</div> </div>
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts" name="WxMsg"> <script setup lang="ts" name="WxMsg">
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxReplySelect from '@/views/mp/components/wx-reply'
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVideoPlayer from '@/views/mp/components/wx-video-play'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news'
import WxLocation from '@/views/mp/components/wx-location/main.vue' import WxLocation from '@/views/mp/components/wx-location'
import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxMusic from '@/views/mp/components/wx-music'
import { getMessagePage, sendMessage } from '@/api/mp/message' import { getMessagePage, sendMessage } from '@/api/mp/message'
import { getUser } from '@/api/mp/user' import { getUser } from '@/api/mp/user'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
@ -187,14 +187,14 @@ const mp: Mp = reactive({
// ========= ========= // ========= =========
const sendLoading = ref(false) // const sendLoading = ref(false) //
interface ObjData { interface Reply {
type: MsgType type: MsgType
accountId: number | null accountId: number | null
articles: any[] articles: any[]
} }
// //
const objData: ObjData = reactive({ const reply = ref<Reply>({
type: MsgType.Text, type: MsgType.Text,
accountId: null, accountId: null,
articles: [] articles: []
@ -209,23 +209,23 @@ onMounted(async () => {
user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar
user.accountId = data.accountId user.accountId = data.accountId
queryParams.accountId = data.accountId queryParams.accountId = data.accountId
objData.accountId = data.accountId reply.value.accountId = data.accountId
refreshChange() refreshChange()
}) })
// //
const sendMsg = async () => { const sendMsg = async () => {
if (!objData) { if (!reply) {
return return
} }
// //
if (objData.type === MsgType.News && objData.articles.length > 1) { if (reply.value.type === MsgType.News && reply.value.articles.length > 1) {
objData.articles = [objData.articles[0]] reply.value.articles = [reply.value.articles[0]]
message.success('图文消息条数限制在 1 条以内,已默认发送第一条') message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
} }
const data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData })) const data = await sendMessage({ userId: props.userId, ...reply.value })
sendLoading.value = false sendLoading.value = false
list.value = [...list.value, ...[data]] list.value = [...list.value, ...[data]]

View File

@ -0,0 +1,3 @@
import WxMusic from './main.vue'
export default WxMusic

View File

@ -56,5 +56,5 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc */ /* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc */
@import '../wx-msg/card.scss'; @import url('../wx-msg/card.scss');
</style> </style>

View File

@ -0,0 +1,3 @@
import WxNews from './main.vue'
export default WxNews

View File

@ -39,12 +39,14 @@
</template> </template>
<script lang="ts" name="WxNews" setup> <script lang="ts" name="WxNews" setup>
const props = defineProps({ const props = withDefaults(
articles: { defineProps<{
type: Array, articles: any[] | null
default: () => null }>(),
{
articles: null
} }
}) )
defineExpose({ defineExpose({
articles: props.articles articles: props.articles
@ -53,9 +55,9 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
.news-home { .news-home {
background-color: #ffffff;
width: 100%; width: 100%;
margin: auto; margin: auto;
background-color: #fff;
} }
.news-main { .news-main {
@ -64,29 +66,29 @@ defineExpose({
} }
.news-content { .news-content {
background-color: #acadae;
width: 100%;
position: relative; position: relative;
width: 100%;
background-color: #acadae;
} }
.news-content-title { .news-content-title {
display: inline-block;
font-size: 12px;
color: #ffffff;
position: absolute; position: absolute;
left: 0;
bottom: 0; bottom: 0;
background-color: black; left: 0;
display: inline-block;
width: 98%; width: 98%;
padding: 1%; padding: 1%;
opacity: 0.65; font-size: 12px;
color: #fff;
white-space: normal; white-space: normal;
background-color: black;
opacity: 0.65;
box-sizing: unset !important; box-sizing: unset !important;
} }
.news-main-item { .news-main-item {
background-color: #ffffff;
padding: 5px 0; padding: 5px 0;
background-color: #fff;
border-top: 1px solid #eaeaea; border-top: 1px solid #eaeaea;
} }
@ -96,17 +98,17 @@ defineExpose({
.news-content-item-title { .news-content-item-title {
display: inline-block; display: inline-block;
font-size: 10px;
width: 70%; width: 70%;
margin-left: 1%; margin-left: 1%;
font-size: 10px;
white-space: normal; white-space: normal;
} }
.news-content-item-img { .news-content-item-img {
display: inline-block; display: inline-block;
width: 25%; width: 25%;
background-color: #acadae;
margin-right: 1%; margin-right: 1%;
background-color: #acadae;
} }
.material-img { .material-img {

View File

@ -1,12 +1,9 @@
<template> <template>
<el-tab-pane name="image"> <div>
<template #label>
<el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 图片</el-row>
</template>
<!-- 情况一已经选择好素材或者上传好图片 --> <!-- 情况一已经选择好素材或者上传好图片 -->
<div class="select-item" v-if="objData.url"> <div class="select-item" v-if="reply.url">
<img class="material-img" :src="objData.url" /> <img class="material-img" :src="reply.url" />
<p class="item-name" v-if="objData.name">{{ objData.name }}</p> <p class="item-name" v-if="reply.name">{{ reply.name }}</p>
<el-row class="ope-row" justify="center"> <el-row class="ope-row" justify="center">
<el-button type="danger" circle @click="onDelete"> <el-button type="danger" circle @click="onDelete">
<Icon icon="ep:delete" /> <Icon icon="ep:delete" />
@ -27,7 +24,11 @@
append-to-body append-to-body
destroy-on-close destroy-on-close
> >
<WxMaterialSelect :objData="objData" @select-material="selectMaterial" /> <WxMaterialSelect
type="image"
:account-id="reply.accountId"
@select-material="selectMaterial"
/>
</el-dialog> </el-dialog>
</el-col> </el-col>
<!-- 文件上传 --> <!-- 文件上传 -->
@ -51,27 +52,27 @@
</el-upload> </el-upload>
</el-col> </el-col>
</el-row> </el-row>
</el-tab-pane> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload' import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import type { UploadRawFile } from 'element-plus' import type { UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth' import { getAccessToken } from '@/utils/auth'
import { ObjData } from './types' import { Reply } from './types'
const message = useMessage() const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary' const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } //
const props = defineProps<{ const props = defineProps<{
modelValue: ObjData modelValue: Reply
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData) (e: 'update:modelValue', v: Reply)
}>() }>()
const objData = computed<ObjData>({ const reply = computed<Reply>({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => emit('update:modelValue', val) set: (val) => emit('update:modelValue', val)
}) })
@ -79,14 +80,13 @@ const objData = computed<ObjData>({
const showDialog = ref(false) const showDialog = ref(false)
const fileList = ref([]) const fileList = ref([])
const uploadData = reactive({ const uploadData = reactive({
accountId: objData.value.accountId, accountId: reply.value.accountId,
type: 'image', type: 'image',
title: '', title: '',
introduction: '' introduction: ''
}) })
const beforeImageUpload = (rawFile: UploadRawFile) => const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
useBeforeUpload(MaterialType.Image, 2)(rawFile)
const onUploadSuccess = (res: any) => { const onUploadSuccess = (res: any) => {
if (res.code !== 0) { if (res.code !== 0) {
@ -104,18 +104,18 @@ const onUploadSuccess = (res: any) => {
} }
const onDelete = () => { const onDelete = () => {
objData.value.mediaId = null reply.value.mediaId = null
objData.value.url = null reply.value.url = null
objData.value.name = null reply.value.name = null
} }
const selectMaterial = (item) => { const selectMaterial = (item) => {
showDialog.value = false showDialog.value = false
objData.value.type = 'image' // reply.value.type = 'image'
objData.value.mediaId = item.mediaId reply.value.mediaId = item.mediaId
objData.value.url = item.url reply.value.url = item.url
objData.value.name = item.name reply.value.name = item.name
} }
</script> </script>
@ -123,7 +123,7 @@ const selectMaterial = (item) => {
.select-item { .select-item {
width: 280px; width: 280px;
padding: 10px; padding: 10px;
margin: 0 auto 10px auto; margin: 0 auto 10px;
border: 1px solid #eaeaea; border: 1px solid #eaeaea;
.material-img { .material-img {
@ -131,11 +131,11 @@ const selectMaterial = (item) => {
} }
.item-name { .item-name {
font-size: 12px;
overflow: hidden; overflow: hidden;
font-size: 12px;
text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
text-align: center;
.item-infos { .item-infos {
width: 30%; width: 30%;
@ -149,18 +149,18 @@ const selectMaterial = (item) => {
} }
.col-select { .col-select {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%; width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
} }
.col-add { .col-add {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
float: right; float: right;
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
.el-upload__tip { .el-upload__tip {
line-height: 18px; line-height: 18px;

View File

@ -1,14 +1,11 @@
<template> <template>
<el-tab-pane name="music"> <div>
<template #label>
<el-row align="middle"><Icon icon="ep:service" />音乐</el-row>
</template>
<el-row align="middle" justify="center"> <el-row align="middle" justify="center">
<el-col :span="6"> <el-col :span="6">
<el-row align="middle" justify="center" class="thumb-div"> <el-row align="middle" justify="center" class="thumb-div">
<el-col :span="24"> <el-col :span="24">
<el-row align="middle" justify="center"> <el-row align="middle" justify="center">
<img style="width: 100px" v-if="objData.thumbMediaUrl" :src="objData.thumbMediaUrl" /> <img style="width: 100px" v-if="reply.thumbMediaUrl" :src="reply.thumbMediaUrl" />
<icon v-else icon="ep:plus" /> <icon v-else icon="ep:plus" />
</el-row> </el-row>
<el-row align="middle" justify="center" style="margin-top: 2%"> <el-row align="middle" justify="center" style="margin-top: 2%">
@ -42,30 +39,31 @@
destroy-on-close destroy-on-close
> >
<WxMaterialSelect <WxMaterialSelect
:objData="{ type: 'image', accountId: objData.accountId }" type="image"
:account-id="reply.accountId"
@select-material="selectMaterial" @select-material="selectMaterial"
/> />
</el-dialog> </el-dialog>
</el-col> </el-col>
<el-col :span="18"> <el-col :span="18">
<el-input v-model="objData.title" placeholder="请输入标题" /> <el-input v-model="reply.title" placeholder="请输入标题" />
<div style="margin: 20px 0"></div> <div style="margin: 20px 0"></div>
<el-input v-model="objData.description" placeholder="请输入描述" /> <el-input v-model="reply.description" placeholder="请输入描述" />
</el-col> </el-col>
</el-row> </el-row>
<div style="margin: 20px 0"></div> <div style="margin: 20px 0"></div>
<el-input v-model="objData.musicUrl" placeholder="请输入音乐链接" /> <el-input v-model="reply.musicUrl" placeholder="请输入音乐链接" />
<div style="margin: 20px 0"></div> <div style="margin: 20px 0"></div>
<el-input v-model="objData.hqMusicUrl" placeholder="请输入高质量音乐链接" /> <el-input v-model="reply.hqMusicUrl" placeholder="请输入高质量音乐链接" />
</el-tab-pane> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import type { UploadRawFile } from 'element-plus' import type { UploadRawFile } from 'element-plus'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload' import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import { getAccessToken } from '@/utils/auth' import { getAccessToken } from '@/utils/auth'
import { ObjData } from './types' import { Reply } from './types'
const message = useMessage() const message = useMessage()
@ -73,12 +71,12 @@ const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/u
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } //
const props = defineProps<{ const props = defineProps<{
modelValue: ObjData modelValue: Reply
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData) (e: 'update:modelValue', v: Reply)
}>() }>()
const objData = computed<ObjData>({ const reply = computed<Reply>({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => emit('update:modelValue', val) set: (val) => emit('update:modelValue', val)
}) })
@ -86,14 +84,13 @@ const objData = computed<ObjData>({
const showDialog = ref(false) const showDialog = ref(false)
const fileList = ref([]) const fileList = ref([])
const uploadData = reactive({ const uploadData = reactive({
accountId: objData.value.accountId, accountId: reply.value.accountId,
type: 'thumb', // thumb type: 'thumb', // thumb
title: '', title: '',
introduction: '' introduction: ''
}) })
const beforeImageUpload = (rawFile: UploadRawFile) => const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
useBeforeUpload(MaterialType.Image, 2)(rawFile)
const onUploadSuccess = (res: any) => { const onUploadSuccess = (res: any) => {
if (res.code !== 0) { if (res.code !== 0) {
@ -113,7 +110,7 @@ const onUploadSuccess = (res: any) => {
const selectMaterial = (item: any) => { const selectMaterial = (item: any) => {
showDialog.value = false showDialog.value = false
objData.value.thumbMediaId = item.mediaId reply.value.thumbMediaId = item.mediaId
objData.value.thumbMediaUrl = item.url reply.value.thumbMediaUrl = item.url
} }
</script> </script>

View File

@ -1,11 +1,8 @@
<template> <template>
<el-tab-pane name="news"> <div>
<template #label>
<el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row>
</template>
<el-row> <el-row>
<div class="select-item" v-if="objData.articles?.length > 0"> <div class="select-item" v-if="reply.articles && reply.articles.length > 0">
<WxNews :articles="objData.articles" /> <WxNews :articles="reply.articles" />
<el-col class="ope-row"> <el-col class="ope-row">
<el-button type="danger" circle @click="onDelete"> <el-button type="danger" circle @click="onDelete">
<Icon icon="ep:delete" /> <Icon icon="ep:delete" />
@ -13,7 +10,7 @@
</el-col> </el-col>
</div> </div>
<!-- 选择素材 --> <!-- 选择素材 -->
<el-col :span="24" v-if="!objData.content"> <el-col :span="24" v-if="!reply.content">
<el-row style="text-align: center" align="middle"> <el-row style="text-align: center" align="middle">
<el-col :span="24"> <el-col :span="24">
<el-button type="success" @click="showDialog = true"> <el-button type="success" @click="showDialog = true">
@ -25,28 +22,29 @@
</el-col> </el-col>
<el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close> <el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close>
<WxMaterialSelect <WxMaterialSelect
:objData="objData" type="news"
@select-material="selectMaterial" :account-id="reply.accountId"
:newsType="newsType" :newsType="newsType"
@select-material="selectMaterial"
/> />
</el-dialog> </el-dialog>
</el-row> </el-row>
</el-tab-pane> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import { ObjData, NewsType } from './types' import { Reply, NewsType } from './types'
const props = defineProps<{ const props = defineProps<{
modelValue: ObjData modelValue: Reply
newsType: NewsType newsType: NewsType
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData) (e: 'update:modelValue', v: Reply)
}>() }>()
const objData = computed<ObjData>({ const reply = computed<Reply>({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => emit('update:modelValue', val) set: (val) => emit('update:modelValue', val)
}) })
@ -55,11 +53,11 @@ const showDialog = ref(false)
const selectMaterial = (item: any) => { const selectMaterial = (item: any) => {
showDialog.value = false showDialog.value = false
objData.value.articles = item.content.newsItem reply.value.articles = item.content.newsItem
} }
const onDelete = () => { const onDelete = () => {
objData.value.articles = [] reply.value.articles = []
} }
</script> </script>
@ -67,7 +65,7 @@ const onDelete = () => {
.select-item { .select-item {
width: 280px; width: 280px;
padding: 10px; padding: 10px;
margin: 0 auto 10px auto; margin: 0 auto 10px;
border: 1px solid #eaeaea; border: 1px solid #eaeaea;
.ope-row { .ope-row {

View File

@ -1,15 +1,10 @@
<template> <template>
<el-tab-pane name="text"> <el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="content" />
<template #label>
<el-row align="middle"><Icon icon="ep:document" /> 文本</el-row>
</template>
<el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="content" />
</el-tab-pane>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const props = defineProps<{
modelValue: string | null modelValue?: string | null
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -17,7 +12,7 @@ const emit = defineEmits<{
(e: 'input', v: string | null) (e: 'input', v: string | null)
}>() }>()
const content = computed<string | null>({ const content = computed<string | null | undefined>({
get: () => props.modelValue, get: () => props.modelValue,
set: (val: string | null) => { set: (val: string | null) => {
emit('update:modelValue', val) emit('update:modelValue', val)

View File

@ -1,17 +1,10 @@
<template> <template>
<el-tab-pane name="video"> <div>
<template #label>
<el-row align="middle"><Icon icon="ep:share" /> 视频</el-row>
</template>
<el-row> <el-row>
<el-input v-model="objData.title" class="input-margin-bottom" placeholder="请输入标题" /> <el-input v-model="reply.title" class="input-margin-bottom" placeholder="请输入标题" />
<el-input <el-input class="input-margin-bottom" v-model="reply.description" placeholder="请输入描述" />
class="input-margin-bottom"
v-model="objData.description"
placeholder="请输入描述"
/>
<el-row class="ope-row" justify="center"> <el-row class="ope-row" justify="center">
<WxVideoPlayer v-if="objData.url" :url="objData.url" /> <WxVideoPlayer v-if="reply.url" :url="reply.url" />
</el-row> </el-row>
<el-col> <el-col>
<el-row style="text-align: center" align="middle"> <el-row style="text-align: center" align="middle">
@ -27,7 +20,11 @@
append-to-body append-to-body
destroy-on-close destroy-on-close
> >
<WxMaterialSelect :objData="objData" @select-material="selectMaterial" /> <WxMaterialSelect
type="video"
:account-id="reply.accountId"
@select-material="selectMaterial"
/>
</el-dialog> </el-dialog>
</el-col> </el-col>
<!-- 文件上传 --> <!-- 文件上传 -->
@ -48,16 +45,16 @@
</el-row> </el-row>
</el-col> </el-col>
</el-row> </el-row>
</el-tab-pane> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVideoPlayer from '@/views/mp/components/wx-video-play'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import type { UploadRawFile } from 'element-plus' import type { UploadRawFile } from 'element-plus'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload' import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import { getAccessToken } from '@/utils/auth' import { getAccessToken } from '@/utils/auth'
import { ObjData } from './types' import { Reply } from './types'
const message = useMessage() const message = useMessage()
@ -65,12 +62,12 @@ const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/u
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } const HEADERS = { Authorization: 'Bearer ' + getAccessToken() }
const props = defineProps<{ const props = defineProps<{
modelValue: ObjData modelValue: Reply
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData) (e: 'update:modelValue', v: Reply)
}>() }>()
const objData = computed<ObjData>({ const reply = computed<Reply>({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => emit('update:modelValue', val) set: (val) => emit('update:modelValue', val)
}) })
@ -78,14 +75,13 @@ const objData = computed<ObjData>({
const showDialog = ref(false) const showDialog = ref(false)
const fileList = ref([]) const fileList = ref([])
const uploadData = reactive({ const uploadData = reactive({
accountId: objData.value.accountId, accountId: reply.value.accountId,
type: 'video', type: 'video',
title: '', title: '',
introduction: '' introduction: ''
}) })
const beforeVideoUpload = (rawFile: UploadRawFile) => const beforeVideoUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Video, 10)(rawFile)
useBeforeUpload(MaterialType.Video, 10)(rawFile)
const onUploadSuccess = (res: any) => { const onUploadSuccess = (res: any) => {
if (res.code !== 0) { if (res.code !== 0) {
@ -105,16 +101,16 @@ const onUploadSuccess = (res: any) => {
const selectMaterial = (item: any) => { const selectMaterial = (item: any) => {
showDialog.value = false showDialog.value = false
objData.value.mediaId = item.mediaId reply.value.mediaId = item.mediaId
objData.value.url = item.url reply.value.url = item.url
objData.value.name = item.name reply.value.name = item.name
// titleintroduction item tempObjItem titleintroduction // titleintroduction item tempObjItem titleintroduction
if (item.title) { if (item.title) {
objData.value.title = item.title || '' reply.value.title = item.title || ''
} }
if (item.introduction) { if (item.introduction) {
objData.value.description = item.introduction || '' reply.value.description = item.introduction || ''
} }
} }
</script> </script>

View File

@ -1,12 +1,9 @@
<template> <template>
<el-tab-pane name="voice"> <div>
<template #label> <div class="select-item2" v-if="reply.url">
<el-row align="middle"><Icon icon="ep:phone" /> 语音</el-row> <p class="item-name">{{ reply.name }}</p>
</template>
<div class="select-item2" v-if="objData.url">
<p class="item-name">{{ objData.name }}</p>
<el-row class="ope-row" justify="center"> <el-row class="ope-row" justify="center">
<WxVoicePlayer :url="objData.url" /> <WxVoicePlayer :url="reply.url" />
</el-row> </el-row>
<el-row class="ope-row" justify="center"> <el-row class="ope-row" justify="center">
<el-button type="danger" circle @click="onDelete"><Icon icon="ep:delete" /></el-button> <el-button type="danger" circle @click="onDelete"><Icon icon="ep:delete" /></el-button>
@ -25,7 +22,11 @@
append-to-body append-to-body
destroy-on-close destroy-on-close
> >
<WxMaterialSelect :objData="objData" @select-material="selectMaterial" /> <WxMaterialSelect
type="voice"
:account-id="reply.accountId"
@select-material="selectMaterial"
/>
</el-dialog> </el-dialog>
</el-col> </el-col>
<!-- 文件上传 --> <!-- 文件上传 -->
@ -49,27 +50,27 @@
</el-upload> </el-upload>
</el-col> </el-col>
</el-row> </el-row>
</el-tab-pane> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload' import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import type { UploadRawFile } from 'element-plus' import type { UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth' import { getAccessToken } from '@/utils/auth'
import { ObjData } from './types' import { Reply } from './types'
const message = useMessage() const message = useMessage()
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary' const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } //
const props = defineProps<{ const props = defineProps<{
modelValue: ObjData modelValue: Reply
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', v: ObjData) (e: 'update:modelValue', v: Reply)
}>() }>()
const objData = computed<ObjData>({ const reply = computed<Reply>({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => emit('update:modelValue', val) set: (val) => emit('update:modelValue', val)
}) })
@ -77,14 +78,13 @@ const objData = computed<ObjData>({
const showDialog = ref(false) const showDialog = ref(false)
const fileList = ref([]) const fileList = ref([])
const uploadData = reactive({ const uploadData = reactive({
accountId: objData.value.accountId, accountId: reply.value.accountId,
type: 'voice', type: 'voice',
title: '', title: '',
introduction: '' introduction: ''
}) })
const beforeVoiceUpload = (rawFile: UploadRawFile) => const beforeVoiceUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Voice, 10)(rawFile)
useBeforeUpload(MaterialType.Voice, 10)(rawFile)
const onUploadSuccess = (res: any) => { const onUploadSuccess = (res: any) => {
if (res.code !== 0) { if (res.code !== 0) {
@ -102,33 +102,33 @@ const onUploadSuccess = (res: any) => {
} }
const onDelete = () => { const onDelete = () => {
objData.value.mediaId = null reply.value.mediaId = null
objData.value.url = null reply.value.url = null
objData.value.name = null reply.value.name = null
} }
const selectMaterial = (item: ObjData) => { const selectMaterial = (item: Reply) => {
showDialog.value = false showDialog.value = false
objData.value.type = 'voice' // reply.value.type = ReplyType.Voice
objData.value.mediaId = item.mediaId reply.value.mediaId = item.mediaId
objData.value.url = item.url reply.value.url = item.url
objData.value.name = item.name reply.value.name = item.name
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.select-item2 { .select-item2 {
padding: 10px; padding: 10px;
margin: 0 auto 10px auto; margin: 0 auto 10px;
border: 1px solid #eaeaea; border: 1px solid #eaeaea;
.item-name { .item-name {
font-size: 12px;
overflow: hidden; overflow: hidden;
font-size: 12px;
text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
text-align: center;
.ope-row { .ope-row {
width: 100%; width: 100%;
@ -138,18 +138,18 @@ const selectMaterial = (item: ObjData) => {
} }
.col-select { .col-select {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%; width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
} }
.col-add { .col-add {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
float: right; float: right;
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
.el-upload__tip { .el-upload__tip {
line-height: 18px; line-height: 18px;

View File

@ -1,25 +1,54 @@
type ReplyType = '' | 'news' | 'image' | 'voice' | 'video' | 'music' | 'text' enum ReplyType {
News = 'news',
Image = 'image',
Voice = 'voice',
Video = 'video',
Music = 'music',
Text = 'text'
}
interface ObjData { interface _Reply {
accountId: number accountId: number
type: ReplyType type: ReplyType
name: string | null name?: string | null
content: string | null content?: string | null
mediaId: string | null mediaId?: string | null
url: string | null url?: string | null
title: string | null title?: string | null
description: string | null description?: string | null
thumbMediaId: string | null thumbMediaId?: string | null
thumbMediaUrl: string | null thumbMediaUrl?: string | null
musicUrl: string | null musicUrl?: string | null
hqMusicUrl: string | null hqMusicUrl?: string | null
introduction: string | null introduction?: string | null
articles: any[] articles?: any[]
} }
type Reply = _Reply //Partial<_Reply>
enum NewsType { enum NewsType {
Published = '1', Published = '1',
Draft = '2' Draft = '2'
} }
export { ObjData, NewsType } /** 利用旧的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 }

View File

@ -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

View File

@ -8,24 +8,59 @@
支持发送视频消息时支持新建视频 支持发送视频消息时支持新建视频
--> -->
<template> <template>
<el-tabs type="border-card" v-model="objData.type" @tab-click="onTabClick"> <el-tabs type="border-card" v-model="currentTab">
<!-- 类型 1文本 --> <!-- 类型 1文本 -->
<TabText v-model="objData.content" /> <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图片 --> <!-- 类型 2图片 -->
<TabImage v-model="objData" /> <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语音 --> <!-- 类型 3语音 -->
<TabVoice v-model="objData" /> <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视频 --> <!-- 类型 4视频 -->
<TabVideo v-model="objData" /> <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图文 --> <!-- 类型 5图文 -->
<TabNews v-model="objData" :news-type="newsType" /> <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音乐 --> <!-- 类型 6音乐 -->
<TabMusic v-model="objData" /> <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> </el-tabs>
</template> </template>
<script setup lang="ts" name="WxReplySelect"> <script setup lang="ts" name="WxReplySelect">
import { ObjData, NewsType } from './components/types' import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types'
import TabText from './components/TabText.vue' import TabText from './components/TabText.vue'
import TabImage from './components/TabImage.vue' import TabImage from './components/TabImage.vue'
import TabVoice from './components/TabVoice.vue' import TabVoice from './components/TabVoice.vue'
@ -34,30 +69,54 @@ import TabNews from './components/TabNews.vue'
import TabMusic from './components/TabMusic.vue' import TabMusic from './components/TabMusic.vue'
interface Props { interface Props {
objData: ObjData modelValue: Reply
newsType?: NewsType newsType?: NewsType
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
newsType: () => NewsType.Published newsType: () => NewsType.Published
}) })
const emit = defineEmits<{
(e: 'update:modelValue', v: Reply)
}>()
const objData = reactive(props.objData) const reply = computed<Reply>({
// TODO @Dhb52Tab 12 TabXXX Form objData MusicMessageFormImageMessageForm get: () => props.modelValue,
// const tempObj = new Map().set(objData.type, Object.assign({}, objData)) set: (val) => emit('update:modelValue', val)
})
// Reply
const tabCache = new Map<ReplyType, Reply>()
// reftabwatchreply
const currentTab = ref<ReplyType>(props.modelValue.type || ReplyType.Text)
/** 切换消息类型的 tab */ watch(
const onTabClick = () => { currentTab,
clear() (newTab, oldTab) => {
} // oldTab undefined
// newTab Reply Partial
if (oldTab === undefined || newTab === undefined) {
return
}
/** 清除除了`type`的字段 */ tabCache.set(oldTab, unref(reply))
// tabReplyReply
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 = () => { const clear = () => {
objData.content = '' reply.value = createEmptyReply(reply)
objData.mediaId = ''
objData.url = ''
objData.title = ''
objData.description = ''
objData.articles = []
} }
defineExpose({ defineExpose({
@ -69,13 +128,13 @@ defineExpose({
.select-item { .select-item {
width: 280px; width: 280px;
padding: 10px; padding: 10px;
margin: 0 auto 10px auto; margin: 0 auto 10px;
border: 1px solid #eaeaea; border: 1px solid #eaeaea;
} }
.select-item2 { .select-item2 {
padding: 10px; padding: 10px;
margin: 0 auto 10px auto; margin: 0 auto 10px;
border: 1px solid #eaeaea; border: 1px solid #eaeaea;
} }
@ -89,11 +148,11 @@ defineExpose({
} }
.item-name { .item-name {
font-size: 12px;
overflow: hidden; overflow: hidden;
font-size: 12px;
text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
text-align: center;
} }
.el-form-item__content { .el-form-item__content {
@ -101,34 +160,34 @@ defineExpose({
} }
.col-select { .col-select {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%; width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
} }
.col-select2 { .col-select2 {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px; height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
} }
.col-add { .col-add {
border: 1px solid rgb(234, 234, 234);
padding: 50px 0px;
height: 160px;
width: 49.5%;
float: right; float: right;
width: 49.5%;
height: 160px;
padding: 50px 0;
border: 1px solid rgb(234 234 234);
} }
.avatar-uploader-icon { .avatar-uploader-icon {
border: 1px solid #d9d9d9;
font-size: 28px;
color: #8c939d;
width: 100px !important; width: 100px !important;
height: 100px !important; height: 100px !important;
font-size: 28px;
line-height: 100px !important; line-height: 100px !important;
color: #8c939d;
text-align: center; text-align: center;
border: 1px solid #d9d9d9;
} }
.material-img { .material-img {

View File

@ -0,0 +1,3 @@
import WxVideoPlayer from './main.vue'
export default WxVideoPlayer

View File

@ -0,0 +1,3 @@
import WxVoicePlayer from './main.vue'
export default WxVoicePlayer

View File

@ -7,7 +7,7 @@
1joolun 的做法使用 mediaId 从微信公众号下载对应的 mp4 素材从而播放内容 1joolun 的做法使用 mediaId 从微信公众号下载对应的 mp4 素材从而播放内容
存在的问题mediaId 有效期是 3 超过时间后无法播放 存在的问题mediaId 有效期是 3 超过时间后无法播放
2重构后的做法后端接收到微信公众号的视频消息后将视频消息的 media_id 的文件内容保存到文件服务器中这样前端可以直接使用 URL 播放 2重构后的做法后端接收到微信公众号的视频消息后将视频消息的 media_id 的文件内容保存到文件服务器中这样前端可以直接使用 URL 播放
代码优化 props 中的 objData 调成为 data 中对应的属性并补充相关注释 代码优化 props 中的 reply 调成为 data 中对应的属性并补充相关注释
--> -->
<template> <template>
<div class="wx-voice-div" @click="playVoice"> <div class="wx-voice-div" @click="playVoice">
@ -86,18 +86,18 @@ const amrStop = () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.wx-voice-div { .wx-voice-div {
display: flex;
width: 120px;
height: 50px;
padding: 5px; padding: 5px;
background-color: #eaeaea; background-color: #eaeaea;
border-radius: 10px; border-radius: 10px;
width: 40px;
height: 40px;
display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.amr-duration { .amr-duration {
font-size: 11px;
margin-left: 5px; margin-left: 5px;
font-size: 11px;
} }
</style> </style>

View File

@ -27,9 +27,7 @@
:on-success="onUploadSuccess" :on-success="onUploadSuccess"
> >
<template #trigger> <template #trigger>
<el-button size="small" type="primary" :loading="isUploading" disabled="isUploading"> <el-button size="small" type="primary">本地上传</el-button>
{{ isUploading ? '正在上传' : '本地上传' }}
</el-button>
</template> </template>
<el-button <el-button
size="small" size="small"
@ -52,7 +50,8 @@
destroy-on-close destroy-on-close
> >
<WxMaterialSelect <WxMaterialSelect
:objData="{ type: 'image', accountId: accountId }" type="image"
:account-id="accountId"
@select-material="onMaterialSelected" @select-material="onMaterialSelected"
/> />
</el-dialog> </el-dialog>
@ -61,13 +60,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import { getAccessToken } from '@/utils/auth' import { getAccessToken } from '@/utils/auth'
import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus' import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
import { NewsItem } from './types' import { NewsItem } from './types'
const message = useMessage() const message = useMessage()
// const UPLOAD_URL = 'http://localhost:8000/upload/' //
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' //
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } //
@ -93,14 +92,13 @@ const showImageDialog = ref(false)
const fileList = ref<UploadFiles>([]) const fileList = ref<UploadFiles>([])
interface UploadData { interface UploadData {
type: 'image' | 'video' | 'audio' type: UploadType
accountId?: number accountId: number | undefined
} }
const uploadData: UploadData = reactive({ const uploadData: UploadData = reactive({
type: 'image', type: UploadType.Image,
accountId: accountId accountId: accountId
}) })
const isUploading = ref(false)
/** 素材选择完成事件*/ /** 素材选择完成事件*/
const onMaterialSelected = (item: any) => { const onMaterialSelected = (item: any) => {
@ -109,22 +107,8 @@ const onMaterialSelected = (item: any) => {
newsItem.value.thumbUrl = item.url newsItem.value.thumbUrl = item.url
} }
const onBeforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => { const onBeforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
const isType = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'].includes( useBeforeUpload(UploadType.Image, 2)(rawFile)
rawFile.type
)
if (!isType) {
message.error('上传图片格式不对!')
return false
}
if (rawFile.size / 1024 / 1024 > 2) {
message.error('上传图片大小不能超过 2M!')
return false
}
//
return true
}
const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => { const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
if (res.code !== 0) { if (res.code !== 0) {

View File

@ -36,7 +36,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news'
import { Article } from './types' import { Article } from './types'

View File

@ -46,7 +46,6 @@
</ContentWrap> </ContentWrap>
<!-- 添加或修改草稿对话框 --> <!-- 添加或修改草稿对话框 -->
<!-- TODO @Dhb52是不是整个做成一个组件 -->
<el-dialog <el-dialog
:title="isCreating ? '新建图文' : '修改图文'" :title="isCreating ? '新建图文' : '修改图文'"
width="80%" width="80%"
@ -63,7 +62,7 @@
</template> </template>
<script setup lang="ts" name="MpDraft"> <script setup lang="ts" name="MpDraft">
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxAccountSelect from '@/views/mp/components/wx-account-select'
import * as MpDraftApi from '@/api/mp/draft' import * as MpDraftApi from '@/api/mp/draft'
import * as MpFreePublishApi from '@/api/mp/freePublish' import * as MpFreePublishApi from '@/api/mp/freePublish'
import { import {
@ -77,7 +76,7 @@ import {
const message = useMessage() // const message = useMessage() //
const accountId = ref(0) const accountId = ref<number>(0)
provide('accountId', accountId) provide('accountId', accountId)
const loading = ref(true) // const loading = ref(true) //
@ -91,7 +90,7 @@ interface QueryParams {
const queryParams: QueryParams = reactive({ const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: accountId.value accountId: 0
}) })
interface UploadData { interface UploadData {
@ -100,7 +99,7 @@ interface UploadData {
} }
const uploadData: UploadData = reactive({ const uploadData: UploadData = reactive({
type: 'image', type: 'image',
accountId: accountId.value accountId: 0
}) })
// ========== 稿 or ========== // ========== 稿 or ==========

View File

@ -50,8 +50,8 @@
<script lang="ts" setup name="MpFreePublish"> <script lang="ts" setup name="MpFreePublish">
import * as FreePublishApi from '@/api/mp/freePublish' import * as FreePublishApi from '@/api/mp/freePublish'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxAccountSelect from '@/views/mp/components/wx-account-select'
const message = useMessage() // const message = useMessage() //
const { t } = useI18n() // const { t } = useI18n() //
@ -62,16 +62,16 @@ const list = ref<any[]>([]) // 列表的数据
interface QueryParams { interface QueryParams {
pageNo: number pageNo: number
pageSize: number pageSize: number
accountId?: number accountId: number
} }
const queryParams: QueryParams = reactive({ const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: 0
}) })
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id: number | undefined) => { const onAccountChanged = (id: number) => {
queryParams.accountId = id queryParams.accountId = id
getList() getList()
} }
@ -102,19 +102,45 @@ const handleDelete = async (item: any) => {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
p {
color: red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
.ope-row { .ope-row {
padding-top: 5px;
margin-top: 5px; margin-top: 5px;
text-align: center; text-align: center;
border-top: 1px solid #eaeaea; border-top: 1px solid #eaeaea;
padding-top: 5px;
} }
.item-name { .item-name {
font-size: 12px;
overflow: hidden; overflow: hidden;
font-size: 12px;
text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
text-align: center;
} }
.el-upload__tip { .el-upload__tip {
@ -125,8 +151,8 @@ const handleDelete = async (item: any) => {
.left { .left {
display: inline-block; display: inline-block;
width: 35%; width: 35%;
vertical-align: top;
margin-top: 200px; margin-top: 200px;
vertical-align: top;
} }
.right { .right {
@ -136,16 +162,16 @@ const handleDelete = async (item: any) => {
} }
.avatar-uploader { .avatar-uploader {
width: 20%;
display: inline-block; display: inline-block;
width: 20%;
} }
.avatar-uploader .el-upload { .avatar-uploader .el-upload {
border-radius: 6px;
cursor: pointer;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
text-align: unset !important; text-align: unset !important;
cursor: pointer;
border-radius: 6px;
} }
.avatar-uploader .el-upload:hover { .avatar-uploader .el-upload:hover {
@ -153,13 +179,13 @@ const handleDelete = async (item: any) => {
} }
.avatar-uploader-icon { .avatar-uploader-icon {
border: 1px solid #d9d9d9;
font-size: 28px;
color: #8c939d;
width: 120px; width: 120px;
height: 120px; height: 120px;
font-size: 28px;
line-height: 120px; line-height: 120px;
color: #8c939d;
text-align: center; text-align: center;
border: 1px solid #d9d9d9;
} }
.avatar { .avatar {
@ -173,13 +199,14 @@ const handleDelete = async (item: any) => {
} }
.digest { .digest {
width: 60%;
display: inline-block; display: inline-block;
width: 60%;
vertical-align: top; vertical-align: top;
} }
/*新增图文*/ /* 新增图文 */
/*瀑布流样式*/
/* 瀑布流样式 */
.waterfall { .waterfall {
width: 100%; width: 100%;
column-gap: 10px; column-gap: 10px;
@ -198,68 +225,44 @@ p {
line-height: 30px; line-height: 30px;
} }
@media (min-width: 992px) and (max-width: 1300px) { /* 瀑布流样式 */
.waterfall {
column-count: 3;
}
p {
color: red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
/*瀑布流样式*/
.news-main { .news-main {
background-color: #ffffff;
width: 100%; width: 100%;
margin: auto;
height: 120px; height: 120px;
margin: auto;
background-color: #fff;
} }
.news-content { .news-content {
background-color: #acadae; position: relative;
width: 100%; width: 100%;
height: 120px; height: 120px;
position: relative; background-color: #acadae;
} }
.news-content-title { .news-content-title {
display: inline-block;
font-size: 15px;
color: #ffffff;
position: absolute; position: absolute;
left: 0px; bottom: 0;
bottom: 0px; left: 0;
background-color: black; display: inline-block;
width: 98%; width: 98%;
height: 25px;
padding: 1%; padding: 1%;
opacity: 0.65;
overflow: hidden; overflow: hidden;
font-size: 15px;
color: #fff;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
height: 25px; background-color: black;
opacity: 0.65;
} }
.news-main-item { .news-main-item {
background-color: #ffffff;
padding: 5px 0px;
border-top: 1px solid #eaeaea;
width: 100%; width: 100%;
padding: 5px 0;
margin: auto; margin: auto;
background-color: #fff;
border-top: 1px solid #eaeaea;
} }
.news-content-item { .news-content-item {
@ -269,8 +272,8 @@ p {
.news-content-item-title { .news-content-item-title {
display: inline-block; display: inline-block;
font-size: 12px;
width: 70%; width: 70%;
font-size: 12px;
} }
.news-content-item-img { .news-content-item-img {
@ -289,9 +292,9 @@ p {
.news-main-plus { .news-main-plus {
width: 280px; width: 280px;
text-align: center;
margin: auto;
height: 50px; height: 50px;
margin: auto;
text-align: center;
} }
.icon-plus { .icon-plus {
@ -302,15 +305,15 @@ p {
.select-item { .select-item {
width: 60%; width: 60%;
padding: 10px; padding: 10px;
margin: 0 auto 10px auto; margin: 0 auto 10px;
border: 1px solid #eaeaea; border: 1px solid #eaeaea;
} }
.father .child { .father .child {
display: none;
text-align: center;
position: relative; position: relative;
bottom: 25px; bottom: 25px;
display: none;
text-align: center;
} }
.father:hover .child { .father:hover .child {

View File

@ -2,29 +2,29 @@ import type { UploadRawFile } from 'element-plus'
const message = useMessage() // 消息 const message = useMessage() // 消息
enum MaterialType { enum UploadType {
Image = 'image', Image = 'image',
Voice = 'voice', Voice = 'voice',
Video = 'video' Video = 'video'
} }
const useBeforeUpload = (type: MaterialType, maxSizeMB: number) => { const useBeforeUpload = (type: UploadType, maxSizeMB: number) => {
const fn = (rawFile: UploadRawFile): boolean => { const fn = (rawFile: UploadRawFile): boolean => {
let allowTypes: string[] = [] let allowTypes: string[] = []
let name = '' let name = ''
switch (type) { switch (type) {
case MaterialType.Image: case UploadType.Image:
allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'] allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
maxSizeMB = 2 maxSizeMB = 2
name = '图片' name = '图片'
break break
case MaterialType.Voice: case UploadType.Voice:
allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr'] allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr']
maxSizeMB = 2 maxSizeMB = 2
name = '语音' name = '语音'
break break
case MaterialType.Video: case UploadType.Video:
allowTypes = ['video/mp4'] allowTypes = ['video/mp4']
maxSizeMB = 10 maxSizeMB = 10
name = '视频' name = '视频'
@ -47,4 +47,4 @@ const useBeforeUpload = (type: MaterialType, maxSizeMB: number) => {
return fn return fn
} }
export { MaterialType, useBeforeUpload } export { UploadType, useBeforeUpload }

View File

@ -31,30 +31,6 @@ const emit = defineEmits<{
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin-top: 10px;
/* 芋道源码:增加 10px避免顶着上面 */
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
.material-img {
width: 100%;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) { @media (min-width: 992px) and (max-width: 1300px) {
.waterfall { .waterfall {
column-count: 3; column-count: 3;
@ -80,4 +56,28 @@ p {
column-count: 1; column-count: 1;
} }
} }
.waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin-top: 10px;
/* 芋道源码:增加 10px避免顶着上面 */
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
.material-img {
width: 100%;
}
p {
line-height: 30px;
}
</style> </style>

View File

@ -6,14 +6,11 @@
:limit="1" :limit="1"
:file-list="fileList" :file-list="fileList"
:data="uploadData" :data="uploadData"
:on-progress="(isUploading = true)"
:on-error="onUploadError" :on-error="onUploadError"
:before-upload="onBeforeUpload" :before-upload="onBeforeUpload"
:on-success="onUploadSuccess" :on-success="onUploadSuccess"
> >
<el-button type="primary" plain :loading="isUploading" :disabled="isUploading"> <el-button type="primary" plain> 点击上传 </el-button>
{{ isUploading ? '正在上传' : '点击上传' }}
</el-button>
<template #tip> <template #tip>
<span class="el-upload__tip" style="margin-left: 5px"> <span class="el-upload__tip" style="margin-left: 5px">
<slot></slot> <slot></slot>
@ -27,14 +24,14 @@ import {
HEADERS, HEADERS,
UPLOAD_URL, UPLOAD_URL,
UploadData, UploadData,
MaterialType, UploadType,
beforeImageUpload, beforeImageUpload,
beforeVoiceUpload beforeVoiceUpload
} from './upload' } from './upload'
const message = useMessage() const message = useMessage()
const props = defineProps<{ type: MaterialType }>() const props = defineProps<{ type: UploadType }>()
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
const emit = defineEmits<{ const emit = defineEmits<{
@ -42,14 +39,13 @@ const emit = defineEmits<{
}>() }>()
const uploadData: UploadData = reactive({ const uploadData: UploadData = reactive({
type: MaterialType.Image, type: UploadType.Image,
title: '', title: '',
introduction: '' introduction: ''
}) })
const isUploading = ref(false)
/** 上传前检查 */ /** 上传前检查 */
const onBeforeUpload = props.type === MaterialType.Image ? beforeImageUpload : beforeVoiceUpload const onBeforeUpload = props.type === UploadType.Image ? beforeImageUpload : beforeVoiceUpload
/** 上传成功处理 */ /** 上传成功处理 */
const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => { const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
@ -64,7 +60,6 @@ const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
uploadData.introduction = '' uploadData.introduction = ''
message.notifySuccess('上传成功') message.notifySuccess('上传成功')
isUploading.value = false
emit('uploaded') emit('uploaded')
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<el-dialog title="新建视频" v-model="showDialog" width="600px" destroy-on-close> <el-dialog title="新建视频" v-model="showDialog" width="600px">
<el-upload <el-upload
:action="UPLOAD_URL" :action="UPLOAD_URL"
:headers="HEADERS" :headers="HEADERS"
@ -8,7 +8,6 @@
:file-list="fileList" :file-list="fileList"
:data="uploadData" :data="uploadData"
:before-upload="beforeVideoUpload" :before-upload="beforeVideoUpload"
:on-progress="(isUploading = true)"
:on-error="onUploadError" :on-error="onUploadError"
:on-success="onUploadSuccess" :on-success="onUploadSuccess"
ref="uploadVideoRef" ref="uploadVideoRef"
@ -18,12 +17,14 @@
<template #trigger> <template #trigger>
<el-button type="primary" plain>选择视频</el-button> <el-button type="primary" plain>选择视频</el-button>
</template> </template>
<span class="el-upload__tip" style="margin-left: 10px" <template #tip>
>格式支持 MP4文件大小不超过 10MB</span <span class="el-upload__tip" style="margin-left: 10px"
> >格式支持 MP4文件大小不超过 10MB</span
>
</template>
</el-upload> </el-upload>
<el-divider /> <el-divider />
<el-form :model="uploadData" :rules="uploadRules" ref="uploadFormRef" v-loading="isUploading"> <el-form :model="uploadData" :rules="uploadRules" ref="uploadFormRef">
<el-form-item label="标题" prop="title"> <el-form-item label="标题" prop="title">
<el-input <el-input
v-model="uploadData.title" v-model="uploadData.title"
@ -41,9 +42,7 @@
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="showDialog = false"> </el-button> <el-button @click="showDialog = false"> </el-button>
<el-button type="primary" @click="submitVideo" :loading="isUploading" :disabled="isUploading" <el-button type="primary" @click="submitVideo"> </el-button>
> </el-button
>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
@ -56,7 +55,7 @@ import type {
UploadProps, UploadProps,
UploadUserFile UploadUserFile
} from 'element-plus' } from 'element-plus'
import { HEADERS, UploadData, UPLOAD_URL, beforeVideoUpload, MaterialType } from './upload' import { HEADERS, UploadData, UPLOAD_URL, UploadType, beforeVideoUpload } from './upload'
const message = useMessage() const message = useMessage()
@ -85,18 +84,16 @@ const showDialog = computed<boolean>({
} }
}) })
const isUploading = ref(false)
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
const uploadData: UploadData = reactive({ const uploadData: UploadData = reactive({
type: MaterialType.Video, type: UploadType.Video,
title: '', title: '',
introduction: '' introduction: ''
}) })
const uploadFormRef = ref<FormInstance>() const uploadFormRef = ref<FormInstance | null>(null)
const uploadVideoRef = ref<UploadInstance>() const uploadVideoRef = ref<UploadInstance | null>(null)
const submitVideo = () => { const submitVideo = () => {
uploadFormRef.value?.validate((valid) => { uploadFormRef.value?.validate((valid) => {
@ -109,7 +106,6 @@ const submitVideo = () => {
/** 上传成功处理 */ /** 上传成功处理 */
const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => { const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
isUploading.value = false
if (res.code !== 0) { if (res.code !== 0) {
message.error('上传出错:' + res.msg) message.error('上传出错:' + res.msg)
return false return false

View File

@ -39,7 +39,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVideoPlayer from '@/views/mp/components/wx-video-play'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
const props = defineProps<{ const props = defineProps<{

View File

@ -37,7 +37,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
const props = defineProps<{ const props = defineProps<{

View File

@ -1,29 +1,29 @@
import type { UploadProps, UploadRawFile } from 'element-plus' import type { UploadProps, UploadRawFile } from 'element-plus'
import { getAccessToken } from '@/utils/auth' import { getAccessToken } from '@/utils/auth'
import { MaterialType, useBeforeUpload } from '@/views/mp/hooks/useUpload' import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头 const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址 const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
interface UploadData { interface UploadData {
type: MaterialType type: UploadType
title: string title: string
introduction: string introduction: string
} }
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
useBeforeUpload(MaterialType.Image, 2)(rawFile) useBeforeUpload(UploadType.Image, 2)(rawFile)
const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
useBeforeUpload(MaterialType.Voice, 2)(rawFile) useBeforeUpload(UploadType.Voice, 2)(rawFile)
const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
useBeforeUpload(MaterialType.Video, 10)(rawFile) useBeforeUpload(UploadType.Video, 10)(rawFile)
export { export {
HEADERS, HEADERS,
UPLOAD_URL, UPLOAD_URL,
MaterialType, UploadType,
UploadData, UploadData,
beforeImageUpload, beforeImageUpload,
beforeVoiceUpload, beforeVoiceUpload,

View File

@ -12,13 +12,13 @@
<ContentWrap> <ContentWrap>
<el-tabs v-model="type" @tab-change="onTabChange"> <el-tabs v-model="type" @tab-change="onTabChange">
<!-- tab 1图片 --> <!-- tab 1图片 -->
<el-tab-pane :name="MaterialType.Image"> <el-tab-pane :name="UploadType.Image">
<template #label> <template #label>
<span> <Icon icon="ep:picture" />图片 </span> <el-row align="middle"> <Icon icon="ep:picture" />图片 </el-row>
</template> </template>
<UploadFile <UploadFile
v-hasPermi="['mp:material:upload-permanent']" v-hasPermi="['mp:material:upload-permanent']"
:type="MaterialType.Image" :type="UploadType.Image"
@uploaded="getList" @uploaded="getList"
> >
支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M 支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M
@ -35,13 +35,13 @@
</el-tab-pane> </el-tab-pane>
<!-- tab 2语音 --> <!-- tab 2语音 -->
<el-tab-pane :name="MaterialType.Voice"> <el-tab-pane :name="UploadType.Voice">
<template #label> <template #label>
<span> <Icon icon="ep:microphone" />语音 </span> <el-row align="middle"> <Icon icon="ep:microphone" />语音 </el-row>
</template> </template>
<UploadFile <UploadFile
v-hasPermi="['mp:material:upload-permanent']" v-hasPermi="['mp:material:upload-permanent']"
:type="MaterialType.Voice" :type="UploadType.Voice"
@uploaded="getList" @uploaded="getList"
> >
格式支持 mp3/wma/wav/amr文件大小不超过 2M播放长度不超过 60s 格式支持 mp3/wma/wav/amr文件大小不超过 2M播放长度不超过 60s
@ -58,9 +58,9 @@
</el-tab-pane> </el-tab-pane>
<!-- tab 3视频 --> <!-- tab 3视频 -->
<el-tab-pane :name="MaterialType.Video"> <el-tab-pane :name="UploadType.Video">
<template #label> <template #label>
<span> <Icon icon="ep:video-play" /> 视频 </span> <el-row align="middle"> <Icon icon="ep:video-play" /> 视频 </el-row>
</template> </template>
<el-button <el-button
v-hasPermi="['mp:material:upload-permanent']" v-hasPermi="['mp:material:upload-permanent']"
@ -85,17 +85,17 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script lang="ts" setup name="MpMaterial"> <script lang="ts" setup name="MpMaterial">
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxAccountSelect from '@/views/mp/components/wx-account-select'
import ImageTable from './components/ImageTable.vue' import ImageTable from './components/ImageTable.vue'
import VoiceTable from './components/VoiceTable.vue' import VoiceTable from './components/VoiceTable.vue'
import VideoTable from './components/VideoTable.vue' import VideoTable from './components/VideoTable.vue'
import UploadFile from './components/UploadFile.vue' import UploadFile from './components/UploadFile.vue'
import UploadVideo from './components/UploadVideo.vue' import UploadVideo from './components/UploadVideo.vue'
import { MaterialType } from './components/upload' import { UploadType } from './components/upload'
import * as MpMaterialApi from '@/api/mp/material' import * as MpMaterialApi from '@/api/mp/material'
const message = useMessage() // const message = useMessage() //
const type = ref<MaterialType>(MaterialType.Image) // const type = ref<UploadType>(UploadType.Image) //
const loading = ref(false) // const loading = ref(false) //
const list = ref<any[]>([]) // const list = ref<any[]>([]) //
const total = ref(0) // const total = ref(0) //
@ -103,19 +103,19 @@ const total = ref(0) // 数据列表
interface QueryParams { interface QueryParams {
pageNo: number pageNo: number
pageSize: number pageSize: number
accountId?: number accountId: number
permanent: boolean permanent: boolean
} }
const queryParams: QueryParams = reactive({ const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined, accountId: 0,
permanent: true permanent: true
}) })
const showCreateVideo = ref(false) // const showCreateVideo = ref(false) //
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id?: number) => { const onAccountChanged = (id: number) => {
queryParams.accountId = id queryParams.accountId = id
getList() getList()
} }

View File

@ -94,7 +94,8 @@
</div> </div>
<el-dialog title="选择图文" v-model="showNewsDialog" width="80%" destroy-on-close> <el-dialog title="选择图文" v-model="showNewsDialog" width="80%" destroy-on-close>
<WxMaterialSelect <WxMaterialSelect
:objData="{ type: 'news', accountId: props.accountId }" type="news"
:account-id="props.accountId"
@select-material="selectMaterial" @select-material="selectMaterial"
/> />
</el-dialog> </el-dialog>
@ -104,7 +105,7 @@
class="configur_content" class="configur_content"
v-if="menu.type === 'click' || menu.type === 'scancode_waitmsg'" v-if="menu.type === 'click' || menu.type === 'scancode_waitmsg'"
> >
<WxReplySelect v-if="hackResetWxReplySelect" :objData="menu.reply" /> <WxReplySelect v-if="hackResetWxReplySelect" v-model="menu.reply" />
</div> </div>
</div> </div>
</div> </div>
@ -112,15 +113,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' import WxReplySelect from '@/views/mp/components/wx-reply'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news'
import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-material-select'
import menuOptions from './menuOptions' import menuOptions from './menuOptions'
const message = useMessage() const message = useMessage()
const props = defineProps<{ const props = defineProps<{
accountId?: number accountId: number
modelValue: any modelValue: any
isParent: boolean isParent: boolean
}>() }>()
@ -130,7 +131,6 @@ const emit = defineEmits<{
(e: 'update:modelValue', v: any) (e: 'update:modelValue', v: any)
}>() }>()
// TODO @Dhb52 table
const menu = computed({ const menu = computed({
get() { get() {
return props.modelValue return props.modelValue

View File

@ -1,35 +1,55 @@
<template> <template>
<div class="menu_bottom" v-for="(parent, x) of menuList" :key="x"> <draggable
<!-- 一级菜单 --> v-model="menuList"
<div item-key="id"
@click="menuClicked(parent, x)" ghost-class="draggable-ghost"
class="menu_item" :animation="400"
:class="{ active: props.activeIndex === `${x}` }" @end="onDragEnd"
> >
<Icon icon="ep:fold" color="black" />{{ parent.name }} <template #item="{ element: parent, index: x }">
</div> <div class="menu_bottom">
<!-- 以下为二级菜单--> <!-- 一级菜单 -->
<div class="submenu" v-if="parentIndex === x && parent.children">
<div class="subtitle menu_bottom" v-for="(child, y) in parent.children" :key="y">
<div <div
class="menu_subItem" @click="menuClicked(parent, x)"
v-if="parent.children" class="menu_item"
:class="{ active: props.activeIndex === `${x}-${y}` }" :class="{ active: props.activeIndex === `${x}` }"
@click="subMenuClicked(child, x, y)"
> >
{{ child.name }} <Icon icon="ep:fold" color="black" />{{ parent.name }}
</div>
<!-- 以下为二级菜单-->
<div class="submenu" v-if="props.parentIndex === x && parent.children">
<draggable
v-model="parent.children"
item-key="id"
ghost-class="draggable-ghost"
:animation="400"
>
<template #item="{ element: child, index: y }">
<div class="subtitle menu_bottom">
<div
class="menu_subItem"
v-if="parent.children"
:class="{ active: props.activeIndex === `${x}-${y}` }"
@click="subMenuClicked(child, x, y)"
>
{{ child.name }}
</div>
</div>
</template>
</draggable>
<!-- 二级菜单加号 当长度 小于 5 才显示二级菜单的加号 -->
<div
class="menu_bottom menu_addicon"
v-if="!parent.children || parent.children.length < 5"
@click="addSubMenu(x, parent)"
>
<Icon icon="ep:plus" class="plus" />
</div>
</div> </div>
</div> </div>
<!-- 二级菜单加号 当长度 小于 5 才显示二级菜单的加号 --> </template>
<div </draggable>
class="menu_bottom menu_addicon"
v-if="!parent.children || parent.children.length < 5"
@click="addSubMenu(x, parent)"
>
<Icon icon="ep:plus" class="plus" />
</div>
</div>
</div>
<!-- 一级菜单加号 --> <!-- 一级菜单加号 -->
<div class="menu_bottom menu_addicon" v-if="menuList.length < 3" @click="addMenu"> <div class="menu_bottom menu_addicon" v-if="menuList.length < 3" @click="addMenu">
<Icon icon="ep:plus" class="plus" /> <Icon icon="ep:plus" class="plus" />
@ -38,12 +58,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { Menu } from './types' import { Menu } from './types'
import draggable from 'vuedraggable'
const props = defineProps<{ const props = defineProps<{
modelValue: Menu[] modelValue: Menu[]
activeIndex: string activeIndex: string
parentIndex: number parentIndex: number
accountId?: number accountId: number
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -91,9 +112,42 @@ const addSubMenu = (i: number, parent: any) => {
const menuClicked = (parent: Menu, x: number) => { const menuClicked = (parent: Menu, x: number) => {
emit('menu-clicked', parent, x) emit('menu-clicked', parent, x)
} }
const subMenuClicked = (child: Menu, x: number, y: number) => { const subMenuClicked = (child: Menu, x: number, y: number) => {
emit('submenu-clicked', child, x, y) emit('submenu-clicked', child, x, y)
} }
/**
* 处理一级菜单展开后被拖动
*
* @param oldIndex: 一级菜单拖动前的位置
* @param newIndex: 一级菜单拖动后的位置
*/
const onDragEnd = ({ oldIndex, newIndex }) => {
//
if (props.activeIndex === '__MENU_NOT_SELECTED__') {
return
}
let newParent = props.parentIndex
if (props.parentIndex === oldIndex) {
newParent = newIndex
} else if (props.parentIndex === newIndex) {
newParent = oldIndex
} else {
// `props.parentIndex`
// 使`newParent`
let positions = new Array<boolean>(menuList.value.length).fill(false)
positions[props.parentIndex] = true
positions.splice(oldIndex, 1)
positions.splice(newIndex, 0, true)
newParent = positions.indexOf(true)
}
//
const parent = menuList.value[newParent]
emit('menu-clicked', parent, newParent)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -155,4 +209,10 @@ const subMenuClicked = (child: Menu, x: number, y: number) => {
box-sizing: border-box; box-sizing: border-box;
} }
} }
.draggable-ghost {
opacity: 0.5;
background: #f7fafc;
border: 1px solid #4299e1;
}
</style> </style>

View File

@ -53,7 +53,7 @@
</template> </template>
<script lang="ts" setup name="MpMenu"> <script lang="ts" setup name="MpMenu">
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxAccountSelect from '@/views/mp/components/wx-account-select'
import MenuEditor from './components/MenuEditor.vue' import MenuEditor from './components/MenuEditor.vue'
import MenuPreviewer from './components/MenuPreviewer.vue' import MenuPreviewer from './components/MenuPreviewer.vue'
import * as MpMenuApi from '@/api/mp/menu' import * as MpMenuApi from '@/api/mp/menu'
@ -65,8 +65,8 @@ const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__'
// ======================== ======================== // ======================== ========================
const loading = ref(false) // const loading = ref(false) //
const accountId = ref<number | undefined>() const accountId = ref<number>(0)
const accountName = ref<string | undefined>('') const accountName = ref<string>('')
const menuList = ref<Menu[]>([]) const menuList = ref<Menu[]>([])
// ======================== ======================== // ======================== ========================
@ -103,7 +103,7 @@ const tempSelfObj = ref<{
const dialogNewsVisible = ref(false) // const dialogNewsVisible = ref(false) //
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id?: number, name?: string) => { const onAccountChanged = (id: number, name: string) => {
accountId.value = id accountId.value = id
accountName.value = name accountName.value = name
getList() getList()
@ -367,7 +367,6 @@ div {
margin-left: 20px; margin-left: 20px;
background-color: #e8e7e7; background-color: #e8e7e7;
box-sizing: border-box; box-sizing: border-box;
box-sizing: border-box;
} }
} }
</style> </style>

View File

@ -122,11 +122,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' import WxVideoPlayer from '@/views/mp/components/wx-video-play'
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
import WxLocation from '@/views/mp/components/wx-location/main.vue' import WxLocation from '@/views/mp/components/wx-location'
import WxMusic from '@/views/mp/components/wx-music/main.vue' import WxMusic from '@/views/mp/components/wx-music'
import WxNews from '@/views/mp/components/wx-news/main.vue' import WxNews from '@/views/mp/components/wx-news'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import { MsgType } from '@/views/mp/components/wx-msg/types' import { MsgType } from '@/views/mp/components/wx-msg/types'

View File

@ -81,8 +81,8 @@
</template> </template>
<script setup lang="ts" name="MpMessage"> <script setup lang="ts" name="MpMessage">
import * as MpMessageApi from '@/api/mp/message' import * as MpMessageApi from '@/api/mp/message'
import WxMsg from '@/views/mp/components/wx-msg/main.vue' import WxMsg from '@/views/mp/components/wx-msg'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxAccountSelect from '@/views/mp/components/wx-account-select'
import MessageTable from './MessageTable.vue' import MessageTable from './MessageTable.vue'
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import { MsgType } from '@/views/mp/components/wx-msg/types' import { MsgType } from '@/views/mp/components/wx-msg/types'
@ -96,17 +96,17 @@ const list = ref<any[]>([]) // 当前页的列表数据
interface QueryParams { interface QueryParams {
pageNo: number pageNo: number
pageSize: number pageSize: number
openid: string | null openid: string | undefined
accountId: number | null accountId: number
type: MsgType | null type: MsgType | undefined
createTime: string[] | [] createTime: string[] | []
} }
const queryParams: QueryParams = reactive({ const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
openid: null, openid: undefined,
accountId: null, accountId: 0,
type: null, type: undefined,
createTime: [] createTime: []
}) })
const queryFormRef = ref<FormInstance | null>(null) // const queryFormRef = ref<FormInstance | null>(null) //
@ -118,8 +118,8 @@ const messageBox = reactive({
}) })
/** 侦听accountId */ /** 侦听accountId */
const onAccountChanged = (id?: number) => { const onAccountChanged = (id: number) => {
queryParams.accountId = id as number queryParams.accountId = id
handleQuery() handleQuery()
} }

View File

@ -14,10 +14,22 @@
<WxAccountSelect @change="onAccountChanged" /> <WxAccountSelect @change="onAccountChanged" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']"> <el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mp:tag:create']"
:disabled="queryParams.accountId === 0"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> </el-button>
<el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']"> <el-button
type="success"
plain
@click="handleSync"
v-hasPermi="['mp:tag:sync']"
:disabled="queryParams.accountId === 0"
>
<Icon icon="ep:refresh" class="mr-5px" /> 同步 <Icon icon="ep:refresh" class="mr-5px" /> 同步
</el-button> </el-button>
</el-form-item> </el-form-item>
@ -74,28 +86,30 @@
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import * as MpTagApi from '@/api/mp/tag' import * as MpTagApi from '@/api/mp/tag'
import TagForm from './TagForm.vue' import TagForm from './TagForm.vue'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxAccountSelect from '@/views/mp/components/wx-account-select'
const message = useMessage() // const message = useMessage() //
const { t } = useI18n() // const { t } = useI18n() //
const loading = ref(true) // const loading = ref(true) //
const total = ref(0) // const total = ref(0) //
const list = ref<any>([]) // const list = ref<any[]>([]) //
interface QueryParams { interface QueryParams {
pageNo: number pageNo: number
pageSize: number pageSize: number
accountId?: number accountId: number
} }
const queryParams: QueryParams = reactive({ const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined accountId: 0
}) })
const formRef = ref<InstanceType<typeof TagForm> | null>(null) const formRef = ref<InstanceType<typeof TagForm> | null>(null)
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id?: number) => { const onAccountChanged = (id: number) => {
queryParams.pageNo = 1 queryParams.pageNo = 1
queryParams.accountId = id queryParams.accountId = id
getList() getList()
@ -114,8 +128,8 @@ const getList = async () => {
} }
/** 添加/修改操作 */ /** 添加/修改操作 */
const openForm = (type: string, id?: number) => { const openForm = (type: 'create' | 'update', id?: number) => {
formRef.value?.open(type, queryParams.accountId as number, id) formRef.value?.open(type, queryParams.accountId, id)
} }
/** 删除按钮操作 */ /** 删除按钮操作 */

View File

@ -34,7 +34,13 @@
<el-form-item> <el-form-item>
<el-button @click="handleQuery"> <Icon icon="ep:search" />搜索 </el-button> <el-button @click="handleQuery"> <Icon icon="ep:search" />搜索 </el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" />重置 </el-button> <el-button @click="resetQuery"> <Icon icon="ep:refresh" />重置 </el-button>
<el-button type="success" plain @click="handleSync" v-hasPermi="['mp:user:sync']"> <el-button
type="success"
plain
@click="handleSync"
v-hasPermi="['mp:user:sync']"
:disabled="queryParams.accountId === 0"
>
<Icon icon="ep:refresh" class="mr-5px" /> 同步 <Icon icon="ep:refresh" class="mr-5px" /> 同步
</el-button> </el-button>
</el-form-item> </el-form-item>
@ -97,7 +103,7 @@
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import * as MpUserApi from '@/api/mp/user' import * as MpUserApi from '@/api/mp/user'
import * as MpTagApi from '@/api/mp/tag' import * as MpTagApi from '@/api/mp/tag'
import WxAccountSelect from '@/views/mp/components/wx-account-select/main.vue' import WxAccountSelect from '@/views/mp/components/wx-account-select'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import UserForm from './UserForm.vue' import UserForm from './UserForm.vue'
@ -110,14 +116,14 @@ const list = ref<any[]>([]) // 列表的数据
interface QueryParams { interface QueryParams {
pageNo: number pageNo: number
pageSize: number pageSize: number
accountId?: number accountId: number
openid: string | null openid: string | null
nickname: string | null nickname: string | null
} }
const queryParams: QueryParams = reactive({ const queryParams: QueryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
accountId: undefined, accountId: 0,
openid: null, openid: null,
nickname: null nickname: null
}) })
@ -125,7 +131,7 @@ const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单
const tagList = ref<any[]>([]) // const tagList = ref<any[]>([]) //
/** 侦听公众号变化 **/ /** 侦听公众号变化 **/
const onAccountChanged = (id?: number) => { const onAccountChanged = (id: number) => {
queryParams.pageNo = 1 queryParams.pageNo = 1
queryParams.accountId = id queryParams.accountId = id
getList() getList()