commit
4cf734d244
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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 @Dhb52:ObjData 这个类名可以在看看,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
|
|
||||||
}
|
|
||||||
|
@ -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)
|
||||||
|
3
src/views/mp/components/wx-account-select/index.ts
Normal file
3
src/views/mp/components/wx-account-select/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import WxAccountSelect from './main.vue'
|
||||||
|
|
||||||
|
export default WxAccountSelect
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
|
3
src/views/mp/components/wx-location/index.ts
Normal file
3
src/views/mp/components/wx-location/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import WxLocation from './main.vue'
|
||||||
|
|
||||||
|
export default WxLocation
|
6
src/views/mp/components/wx-material-select/index.ts
Normal file
6
src/views/mp/components/wx-material-select/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import WxMaterialSelect from './main.vue'
|
||||||
|
import { NewsType, MaterialType } from './types'
|
||||||
|
|
||||||
|
export { NewsType, MaterialType }
|
||||||
|
|
||||||
|
export default WxMaterialSelect
|
@ -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: {
|
}>(),
|
||||||
// 图文类型:1、已发布图文;2、草稿箱图文
|
{
|
||||||
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>
|
||||||
|
11
src/views/mp/components/wx-material-select/types.ts
Normal file
11
src/views/mp/components/wx-material-select/types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export enum NewsType {
|
||||||
|
Draft = '2',
|
||||||
|
Published = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MaterialType {
|
||||||
|
Image = 'image',
|
||||||
|
Voice = 'voice',
|
||||||
|
Video = 'video',
|
||||||
|
News = 'news'
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
6
src/views/mp/components/wx-msg/index.ts
Normal file
6
src/views/mp/components/wx-msg/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import WxMsg from './main.vue'
|
||||||
|
import { MsgType } from './types'
|
||||||
|
|
||||||
|
export { MsgType }
|
||||||
|
|
||||||
|
export default WxMsg
|
@ -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]]
|
||||||
|
3
src/views/mp/components/wx-music/index.ts
Normal file
3
src/views/mp/components/wx-music/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import WxMusic from './main.vue'
|
||||||
|
|
||||||
|
export default WxMusic
|
@ -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>
|
||||||
|
3
src/views/mp/components/wx-news/index.ts
Normal file
3
src/views/mp/components/wx-news/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import WxNews from './main.vue'
|
||||||
|
|
||||||
|
export default WxNews
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
// title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction
|
// title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction
|
||||||
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>
|
||||||
|
@ -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;
|
||||||
|
@ -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 }
|
||||||
|
7
src/views/mp/components/wx-reply/index.ts
Normal file
7
src/views/mp/components/wx-reply/index.ts
Normal 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
|
@ -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 @Dhb52:Tab 切换的时候,应该表单还保留着;清除只有两个时机:1)发送成功后;2)关闭窗口后;我捉摸,是不是每个 TabXXX 组件,是个独立的 Form,然后有自己的对象,不粘在 objData 一起。这样最终就是 MusicMessageForm、ImageMessageForm
|
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>()
|
||||||
|
// 采用独立的ref来保存当前tab,避免在watch标签变化,对reply进行赋值会产生了循环调用
|
||||||
|
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))
|
||||||
|
|
||||||
|
// 从缓存里面取出新tab内容,有则覆盖Reply,没有则创建空Reply
|
||||||
|
const temp = tabCache.get(newTab)
|
||||||
|
if (temp) {
|
||||||
|
reply.value = temp
|
||||||
|
} else {
|
||||||
|
let newData = createEmptyReply(reply)
|
||||||
|
newData.type = newTab
|
||||||
|
reply.value = newData
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 清除除了`type`, `accountId`的字段 */
|
||||||
const clear = () => {
|
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 {
|
||||||
|
3
src/views/mp/components/wx-video-play/index.ts
Normal file
3
src/views/mp/components/wx-video-play/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import WxVideoPlayer from './main.vue'
|
||||||
|
|
||||||
|
export default WxVideoPlayer
|
3
src/views/mp/components/wx-voice-play/index.ts
Normal file
3
src/views/mp/components/wx-voice-play/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import WxVoicePlayer from './main.vue'
|
||||||
|
|
||||||
|
export default WxVoicePlayer
|
@ -7,7 +7,7 @@
|
|||||||
1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
|
1)joolun 的做法:使用 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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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 修改 ==========
|
||||||
|
@ -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 {
|
||||||
|
@ -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 }
|
||||||
|
@ -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>
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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<{
|
||||||
|
@ -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<{
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user