remove 公众号模块
parent
557374e8ac
commit
d5a4b04b3b
@ -1,46 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface AccountVO {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
// 创建公众号账号
|
||||
export const createAccount = async (data) => {
|
||||
return await request.post({ url: '/mp/account/create', data })
|
||||
}
|
||||
|
||||
// 更新公众号账号
|
||||
export const updateAccount = async (data) => {
|
||||
return request.put({ url: '/mp/account/update', data: data })
|
||||
}
|
||||
|
||||
// 删除公众号账号
|
||||
export const deleteAccount = async (id) => {
|
||||
return request.delete({ url: '/mp/account/delete?id=' + id, method: 'delete' })
|
||||
}
|
||||
|
||||
// 获得公众号账号
|
||||
export const getAccount = async (id) => {
|
||||
return request.get({ url: '/mp/account/get?id=' + id })
|
||||
}
|
||||
|
||||
// 获得公众号账号分页
|
||||
export const getAccountPage = async (query) => {
|
||||
return request.get({ url: '/mp/account/page', params: query })
|
||||
}
|
||||
|
||||
// 获取公众号账号精简信息列表
|
||||
export const getSimpleAccountList = async () => {
|
||||
return request.get({ url: '/mp/account/list-all-simple' })
|
||||
}
|
||||
|
||||
// 生成公众号二维码
|
||||
export const generateAccountQrCode = async (id) => {
|
||||
return request.put({ url: '/mp/account/generate-qr-code?id=' + id })
|
||||
}
|
||||
|
||||
// 清空公众号 API 配额
|
||||
export const clearAccountQuota = async (id) => {
|
||||
return request.put({ url: '/mp/account/clear-quota?id=' + id })
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 创建公众号的自动回复
|
||||
export const createAutoReply = (data) => {
|
||||
return request.post({
|
||||
url: '/mp/auto-reply/create',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新公众号的自动回复
|
||||
export const updateAutoReply = (data) => {
|
||||
return request.put({
|
||||
url: '/mp/auto-reply/update',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除公众号的自动回复
|
||||
export const deleteAutoReply = (id) => {
|
||||
return request.delete({
|
||||
url: '/mp/auto-reply/delete?id=' + id
|
||||
})
|
||||
}
|
||||
|
||||
// 获得公众号的自动回复
|
||||
export const getAutoReply = (id) => {
|
||||
return request.get({
|
||||
url: '/mp/auto-reply/get?id=' + id
|
||||
})
|
||||
}
|
||||
|
||||
// 获得公众号的自动回复分页
|
||||
export const getAutoReplyPage = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/auto-reply/page',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 获得公众号草稿分页
|
||||
export const getDraftPage = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/draft/page',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 创建公众号草稿
|
||||
export const createDraft = (accountId, articles) => {
|
||||
return request.post({
|
||||
url: '/mp/draft/create?accountId=' + accountId,
|
||||
data: {
|
||||
articles
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更新公众号草稿
|
||||
export const updateDraft = (accountId, mediaId, articles) => {
|
||||
return request.put({
|
||||
url: '/mp/draft/update?accountId=' + accountId + '&mediaId=' + mediaId,
|
||||
method: 'put',
|
||||
data: articles
|
||||
})
|
||||
}
|
||||
|
||||
// 删除公众号草稿
|
||||
export const deleteDraft = (accountId, mediaId) => {
|
||||
return request.delete({
|
||||
url: '/mp/draft/delete?accountId=' + accountId + '&mediaId=' + mediaId
|
||||
})
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 获得公众号素材分页
|
||||
export const getFreePublishPage = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/free-publish/page',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 删除公众号素材
|
||||
export const deleteFreePublish = (accountId, articleId) => {
|
||||
return request.delete({
|
||||
url: '/mp/free-publish/delete?accountId=' + accountId + '&articleId=' + articleId
|
||||
})
|
||||
}
|
||||
|
||||
// 发布公众号素材
|
||||
export const submitFreePublish = (accountId, mediaId) => {
|
||||
return request.post({
|
||||
url: '/mp/free-publish/submit?accountId=' + accountId + '&mediaId=' + mediaId
|
||||
})
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 获得公众号素材分页
|
||||
export const getMaterialPage = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/material/page',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 删除公众号永久素材
|
||||
export const deletePermanentMaterial = (id) => {
|
||||
return request.delete({
|
||||
url: '/mp/material/delete-permanent?id=' + id
|
||||
})
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 获得公众号菜单列表
|
||||
export const getMenuList = (accountId) => {
|
||||
return request.get({
|
||||
url: '/mp/menu/list?accountId=' + accountId
|
||||
})
|
||||
}
|
||||
|
||||
// 保存公众号菜单
|
||||
export const saveMenu = (accountId, menus) => {
|
||||
return request.post({
|
||||
url: '/mp/menu/save',
|
||||
data: {
|
||||
accountId,
|
||||
menus
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除公众号菜单
|
||||
export const deleteMenu = (accountId) => {
|
||||
return request.delete({
|
||||
url: '/mp/menu/delete?accountId=' + accountId
|
||||
})
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 获得公众号消息分页
|
||||
export const getMessagePage = (query: PageParam) => {
|
||||
return request.get({
|
||||
url: '/mp/message/page',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 给粉丝发送消息
|
||||
export const sendMessage = (data) => {
|
||||
return request.post({
|
||||
url: '/mp/message/send',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 获取消息发送概况数据
|
||||
export const getUpstreamMessage = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/statistics/upstream-message',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 用户增减数据
|
||||
export const getUserSummary = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/statistics/user-summary',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获得用户累计数据
|
||||
export const getUserCumulate = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/statistics/user-cumulate',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获得接口分析数据
|
||||
export const getInterfaceSummary = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/statistics/interface-summary',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface TagVO {
|
||||
id?: number
|
||||
name: string
|
||||
accountId: number
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
// 创建公众号标签
|
||||
export const createTag = (data: TagVO) => {
|
||||
return request.post({
|
||||
url: '/mp/tag/create',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新公众号标签
|
||||
export const updateTag = (data: TagVO) => {
|
||||
return request.put({
|
||||
url: '/mp/tag/update',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除公众号标签
|
||||
export const deleteTag = (id: number) => {
|
||||
return request.delete({
|
||||
url: '/mp/tag/delete?id=' + id
|
||||
})
|
||||
}
|
||||
|
||||
// 获得公众号标签
|
||||
export const getTag = (id: number) => {
|
||||
return request.get({
|
||||
url: '/mp/tag/get?id=' + id
|
||||
})
|
||||
}
|
||||
|
||||
// 获得公众号标签分页
|
||||
export const getTagPage = (query: PageParam) => {
|
||||
return request.get({
|
||||
url: '/mp/tag/page',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取公众号标签精简信息列表
|
||||
export const getSimpleTagList = () => {
|
||||
return request.get({
|
||||
url: '/mp/tag/list-all-simple'
|
||||
})
|
||||
}
|
||||
|
||||
// 同步公众号标签
|
||||
export const syncTag = (accountId: number) => {
|
||||
return request.post({
|
||||
url: '/mp/tag/sync?accountId=' + accountId
|
||||
})
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 更新公众号粉丝
|
||||
export const updateUser = (data) => {
|
||||
return request.put({
|
||||
url: '/mp/user/update',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 获得公众号粉丝
|
||||
export const getUser = (id) => {
|
||||
return request.get({
|
||||
url: '/mp/user/get?id=' + id
|
||||
})
|
||||
}
|
||||
|
||||
// 获得公众号粉丝分页
|
||||
export const getUserPage = (query) => {
|
||||
return request.get({
|
||||
url: '/mp/user/page',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 同步公众号粉丝
|
||||
export const syncUser = (accountId) => {
|
||||
return request.post({
|
||||
url: '/mp/user/sync?accountId=' + accountId
|
||||
})
|
||||
}
|
||||
@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<doc-alert title="公众号接入" url="https://doc.iocoder.cn/mp/account/" />
|
||||
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
<el-button type="primary" @click="openForm('create')" v-hasPermi="['mp:account:create']">
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="名称" align="center" prop="name" />
|
||||
<el-table-column label="微信号" align="center" prop="account" width="180" />
|
||||
<el-table-column label="appId" align="center" prop="appId" width="180" />
|
||||
<el-table-column label="服务器地址(URL)" align="center" prop="appId" width="360">
|
||||
<template #default="scope">
|
||||
{{ 'http://服务端地址/admin-api/mp/open/' + scope.row.appId }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="二维码" align="center" prop="qrCodeUrl">
|
||||
<template #default="scope">
|
||||
<img
|
||||
v-if="scope.row.qrCodeUrl"
|
||||
:src="scope.row.qrCodeUrl"
|
||||
alt="二维码"
|
||||
style="display: inline-block; height: 100px"
|
||||
/>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleGenerateQrCode(scope.row)"
|
||||
v-hasPermi="['mp:account:qr-code']"
|
||||
>
|
||||
生成二维码
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['mp:account:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['mp:account:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleCleanQuota(scope.row)"
|
||||
v-hasPermi="['mp:account:clear-quota']"
|
||||
>
|
||||
清空 API 配额
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 对话框(添加 / 修改) -->
|
||||
<AccountForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import * as AccountApi from '@/api/mp/account'
|
||||
import AccountForm from './AccountForm.vue'
|
||||
|
||||
defineOptions({ name: 'MpAccount' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
account: null,
|
||||
appId: null
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await AccountApi.getAccountPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await AccountApi.deleteAccount(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 生成二维码的按钮操作 */
|
||||
const handleGenerateQrCode = async (row) => {
|
||||
try {
|
||||
// 生成二维码的二次确认
|
||||
await message.confirm('是否确认生成公众号账号编号为"' + row.name + '"的二维码?')
|
||||
// 发起生成二维码
|
||||
await AccountApi.generateAccountQrCode(row.id)
|
||||
message.success('生成二维码成功')
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 清空二维码 API 配额的按钮操作 */
|
||||
const handleCleanQuota = async (row) => {
|
||||
try {
|
||||
// 清空 API 配额的二次确认
|
||||
await message.confirm('是否确认清空生成公众号账号编号为"' + row.name + '"的 API 配额?')
|
||||
// 发起清空 API 配额
|
||||
await AccountApi.clearAccountQuota(row.id)
|
||||
message.success('清空 API 配额成功')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
|
||||
<el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message">
|
||||
<el-select v-model="replyForm.requestMessageType" placeholder="请选择">
|
||||
<template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value">
|
||||
<el-option
|
||||
v-if="RequestMessageTypes.includes(dict.value)"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
|
||||
<el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
|
||||
<el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="回复消息">
|
||||
<WxReplySelect v-model="reply" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WxReplySelect, { type Reply } from '@/views/mp/components/wx-reply'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { MsgType } from './types'
|
||||
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
|
||||
|
||||
defineOptions({ name: 'ReplyForm' })
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
reply: Reply
|
||||
msgType: MsgType
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:reply', v: Reply)
|
||||
(e: 'update:modelValue', v: any)
|
||||
}>()
|
||||
|
||||
const reply = computed<Reply>({
|
||||
get: () => props.reply,
|
||||
set: (val) => emit('update:reply', val)
|
||||
})
|
||||
|
||||
const replyForm = computed<any>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance | null>(null) // 表单 ref
|
||||
|
||||
const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] // 允许选择的请求消息类型
|
||||
|
||||
// 表单校验
|
||||
const rules = {
|
||||
requestKeyword: [{ required: true, message: '请求的关键字不能为空', trigger: 'blur' }],
|
||||
requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetFields: () => formRef.value?.resetFields(),
|
||||
validate: async () => formRef.value?.validate()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<el-table v-loading="props.loading" :data="props.list">
|
||||
<el-table-column
|
||||
label="请求消息类型"
|
||||
align="center"
|
||||
prop="requestMessageType"
|
||||
v-if="msgType === MsgType.Message"
|
||||
/>
|
||||
<el-table-column
|
||||
label="关键词"
|
||||
align="center"
|
||||
prop="requestKeyword"
|
||||
v-if="msgType === MsgType.Keyword"
|
||||
/>
|
||||
<el-table-column
|
||||
label="匹配类型"
|
||||
align="center"
|
||||
prop="requestMatch"
|
||||
v-if="msgType === MsgType.Keyword"
|
||||
>
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复消息类型" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MP_MESSAGE_TYPE" :value="scope.row.responseMessageType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复内容" align="center">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'voice'">
|
||||
<WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" />
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'image'">
|
||||
<a target="_blank" :href="scope.row.responseMediaUrl">
|
||||
<img :src="scope.row.responseMediaUrl" style="width: 100px" />
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
scope.row.responseMessageType === 'video' ||
|
||||
scope.row.responseMessageType === 'shortvideo'
|
||||
"
|
||||
>
|
||||
<WxVideoPlayer
|
||||
v-if="scope.row.responseMediaUrl"
|
||||
:url="scope.row.responseMediaUrl"
|
||||
style="margin-top: 10px"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'news'">
|
||||
<WxNews :articles="scope.row.responseArticles" />
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'music'">
|
||||
<WxMusic
|
||||
:title="scope.row.responseTitle"
|
||||
:description="scope.row.responseDescription"
|
||||
:thumb-media-url="scope.row.responseThumbMediaUrl"
|
||||
:music-url="scope.row.responseMusicUrl"
|
||||
:hq-music-url="scope.row.responseHqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="emit('on-update', scope.row.id)"
|
||||
v-hasPermi="['mp:auto-reply:update']"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="emit('on-delete', scope.row.id)"
|
||||
v-hasPermi="['mp:auto-reply:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
|
||||
import WxMusic from '@/views/mp/components/wx-music'
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { MsgType } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
loading: boolean
|
||||
list: any[]
|
||||
msgType: MsgType
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'on-update', v: number)
|
||||
(e: 'on-delete', v: number)
|
||||
}>()
|
||||
</script>
|
||||
@ -1,3 +0,0 @@
|
||||
import WxAccountSelect from './main.vue'
|
||||
|
||||
export default WxAccountSelect
|
||||
@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged">
|
||||
<el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as MpAccountApi from '@/api/mp/account'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const { push, currentRoute } = useRouter() // 路由
|
||||
|
||||
defineOptions({ name: 'WxAccountSelect' })
|
||||
|
||||
const account: MpAccountApi.AccountVO = reactive({
|
||||
id: -1,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const accountList = ref<MpAccountApi.AccountVO[]>([])
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', id: number, name: string)
|
||||
}>()
|
||||
|
||||
const handleQuery = async () => {
|
||||
accountList.value = await MpAccountApi.getSimpleAccountList()
|
||||
if (accountList.value.length == 0) {
|
||||
message.error('未配置公众号,请在【公众号管理 -> 账号管理】菜单,进行配置')
|
||||
delView(unref(currentRoute))
|
||||
await push({ name: 'MpAccount' })
|
||||
return
|
||||
}
|
||||
// 默认选中第一个
|
||||
if (accountList.value.length > 0) {
|
||||
account.id = accountList.value[0].id
|
||||
if (account.id) {
|
||||
account.name = accountList.value[0].name
|
||||
emit('change', account.id, account.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onChanged = (id?: number) => {
|
||||
const found = accountList.value.find((v) => v.id === id)
|
||||
if (account.id) {
|
||||
account.name = found ? found.name : ''
|
||||
emit('change', account.id, account.name)
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
handleQuery()
|
||||
})
|
||||
</script>
|
||||
@ -1,3 +0,0 @@
|
||||
import WxLocation from './main.vue'
|
||||
|
||||
export default WxLocation
|
||||
@ -1,73 +0,0 @@
|
||||
<!--
|
||||
【微信消息 - 定位】TODO @Dhb52 目前未启用
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<el-link
|
||||
type="primary"
|
||||
target="_blank"
|
||||
:href="
|
||||
'https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&pointx=' +
|
||||
locationY +
|
||||
'&pointy=' +
|
||||
locationX +
|
||||
'&name=' +
|
||||
label +
|
||||
'&ref=yudao'
|
||||
"
|
||||
>
|
||||
<el-col>
|
||||
<el-row>
|
||||
<img
|
||||
:src="
|
||||
'https://apis.map.qq.com/ws/staticmap/v2/?zoom=10&markers=color:blue|label:A|' +
|
||||
locationX +
|
||||
',' +
|
||||
locationY +
|
||||
'&key=' +
|
||||
qqMapKey +
|
||||
'&size=250*180'
|
||||
"
|
||||
/>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<Icon icon="ep:location" />
|
||||
{{ label }}
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'WxLocation' })
|
||||
|
||||
const props = defineProps({
|
||||
locationX: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
locationY: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
label: {
|
||||
// 地名
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
qqMapKey: {
|
||||
// QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
|
||||
required: false,
|
||||
type: String,
|
||||
default: 'TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E' // 需要自定义
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
locationX: props.locationX,
|
||||
locationY: props.locationY,
|
||||
label: props.label,
|
||||
qqMapKey: props.qqMapKey
|
||||
})
|
||||
</script>
|
||||
@ -1,6 +0,0 @@
|
||||
import WxMaterialSelect from './main.vue'
|
||||
import { NewsType, MaterialType } from './types'
|
||||
|
||||
export { NewsType, MaterialType }
|
||||
|
||||
export default WxMaterialSelect
|
||||
@ -1,11 +0,0 @@
|
||||
export enum NewsType {
|
||||
Draft = '2',
|
||||
Published = '1'
|
||||
}
|
||||
|
||||
export enum MaterialType {
|
||||
Image = 'image',
|
||||
Voice = 'voice',
|
||||
Video = 'video',
|
||||
News = 'news'
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
.avue-card {
|
||||
&__item {
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
list-style: none;
|
||||
font-feature-settings: 'tnum';
|
||||
cursor: pointer;
|
||||
height: 200px;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(0, 0, 0, 0.09);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
|
||||
&--add {
|
||||
border: 1px dashed #000;
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
background-color: #fff;
|
||||
border-color: #d9d9d9;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
background-color: #fff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
&__detail {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 48px;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
&__menu {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
height: 50px;
|
||||
background: #f7f9fa;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** joolun 额外加的 */
|
||||
.avue-comment__main {
|
||||
flex: unset !important;
|
||||
border-radius: 5px !important;
|
||||
margin: 0 8px !important;
|
||||
}
|
||||
|
||||
.avue-comment__header {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.avue-comment__body {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */
|
||||
.avue-comment {
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&--reverse {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.avue-comment__main {
|
||||
&:before,
|
||||
&:after {
|
||||
left: auto;
|
||||
right: -8px;
|
||||
border-width: 8px 0 8px 8px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-left-color: #dedede;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-left-color: #f8f8f8;
|
||||
margin-right: 1px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid transparent;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 5px 15px;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__author {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
margin: 0 20px;
|
||||
position: relative;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 2px;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -8px;
|
||||
right: 100%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: block;
|
||||
content: ' ';
|
||||
border-color: transparent;
|
||||
border-style: solid solid outset;
|
||||
border-width: 8px 8px 8px 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-right-color: #dedede;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-right-color: #f8f8f8;
|
||||
margin-left: 1px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
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-size: 14px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
font-family:
|
||||
Georgia,
|
||||
Times New Roman,
|
||||
Times,
|
||||
Kai,
|
||||
Kaiti SC,
|
||||
KaiTi,
|
||||
BiauKai,
|
||||
FontAwesome,
|
||||
serif;
|
||||
padding: 1px 0 1px 15px;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<MsgEvent v-if="item.type === MsgType.Event" :item="item" />
|
||||
|
||||
<div v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Voice">
|
||||
<WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Image">
|
||||
<a target="_blank" :href="item.mediaUrl">
|
||||
<img :src="item.mediaUrl" style="width: 100px" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'"
|
||||
style="text-align: center"
|
||||
>
|
||||
<WxVideoPlayer :url="item.mediaUrl" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Link" class="avue-card__detail">
|
||||
<el-link type="success" :underline="false" target="_blank" :href="item.url">
|
||||
<div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
|
||||
</el-link>
|
||||
<div class="avue-card__info" style="height: unset">{{ item.description }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Location">
|
||||
<WxLocation :label="item.label" :location-y="item.locationY" :location-x="item.locationX" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.News" style="width: 300px">
|
||||
<WxNews :articles="item.articles" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Music">
|
||||
<WxMusic
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:thumb-media-url="item.thumbMediaUrl"
|
||||
:music-url="item.musicUrl"
|
||||
:hq-music-url="item.hqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
import WxLocation from '@/views/mp/components/wx-location'
|
||||
import WxMusic from '@/views/mp/components/wx-music'
|
||||
import MsgEvent from './MsgEvent.vue'
|
||||
import { MsgType } from '../types'
|
||||
|
||||
defineOptions({ name: 'Msg' })
|
||||
|
||||
const props = defineProps<{
|
||||
item: any
|
||||
}>()
|
||||
|
||||
const item = ref<any>(props.item)
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="item.event === 'subscribe'">
|
||||
<el-tag type="success">关注</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'unsubscribe'">
|
||||
<el-tag type="danger">取消关注</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'CLICK'">
|
||||
<el-tag>点击菜单</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.event === 'VIEW'">
|
||||
<el-tag>点击菜单链接</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.event === 'scancode_waitmsg'">
|
||||
<el-tag>扫码结果</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.event === 'scancode_push'">
|
||||
<el-tag>扫码结果</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.event === 'pic_sysphoto'">
|
||||
<el-tag>系统拍照发图</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'pic_photo_or_album'">
|
||||
<el-tag>拍照或者相册</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'pic_weixin'">
|
||||
<el-tag>微信相册</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'location_select'">
|
||||
<el-tag>选择地理位置</el-tag>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tag type="danger">未知事件类型</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
item: any
|
||||
}>()
|
||||
|
||||
const item = ref(props.item)
|
||||
</script>
|
||||
@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div class="execution" v-for="item in props.list" :key="item.id">
|
||||
<div
|
||||
class="avue-comment"
|
||||
:class="{ 'avue-comment--reverse': item.sendFrom === SendFrom.MpBot }"
|
||||
>
|
||||
<div class="avatar-div">
|
||||
<img :src="getAvatar(item.sendFrom)" class="avue-comment__avatar" />
|
||||
<div class="avue-comment__author">
|
||||
{{ getNickname(item.sendFrom) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="avue-comment__main">
|
||||
<div class="avue-comment__header">
|
||||
<div class="avue-comment__create_time">{{ formatDate(item.createTime) }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="avue-comment__body"
|
||||
:style="item.sendFrom === SendFrom.MpBot ? 'background: #6BED72;' : ''"
|
||||
>
|
||||
<Msg :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Msg from './Msg.vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { User } from '../types'
|
||||
import avatarWechat from '@/assets/imgs/wechat.png'
|
||||
|
||||
defineOptions({ name: 'MsgList' })
|
||||
|
||||
const props = defineProps<{
|
||||
list: any[]
|
||||
accountId: number
|
||||
user: User
|
||||
}>()
|
||||
|
||||
enum SendFrom {
|
||||
User = 1,
|
||||
MpBot = 2
|
||||
}
|
||||
|
||||
const getAvatar = (sendFrom: SendFrom) =>
|
||||
sendFrom === SendFrom.User ? props.user.avatar : avatarWechat
|
||||
|
||||
const getNickname = (sendFrom: SendFrom) =>
|
||||
sendFrom === SendFrom.User ? props.user.nickname : '公众号'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */
|
||||
@import url('../comment.scss');
|
||||
@import url('../card.scss');
|
||||
|
||||
.avatar-div {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +0,0 @@
|
||||
import WxMsg from './main.vue'
|
||||
import { MsgType } from './types'
|
||||
|
||||
export { MsgType }
|
||||
|
||||
export default WxMsg
|
||||
@ -1,17 +0,0 @@
|
||||
export enum MsgType {
|
||||
Event = 'event',
|
||||
Text = 'text',
|
||||
Voice = 'voice',
|
||||
Image = 'image',
|
||||
Video = 'video',
|
||||
Link = 'link',
|
||||
Location = 'location',
|
||||
Music = 'music',
|
||||
News = 'news'
|
||||
}
|
||||
|
||||
export interface User {
|
||||
nickname: string
|
||||
avatar: string
|
||||
accountId: number
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
import WxMusic from './main.vue'
|
||||
|
||||
export default WxMusic
|
||||
@ -1,62 +0,0 @@
|
||||
<!--
|
||||
【微信消息 - 音乐】
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<el-link
|
||||
type="success"
|
||||
:underline="false"
|
||||
target="_blank"
|
||||
:href="hqMusicUrl ? hqMusicUrl : musicUrl"
|
||||
>
|
||||
<div
|
||||
class="avue-card__body"
|
||||
style="padding: 10px; background-color: #fff; border-radius: 5px"
|
||||
>
|
||||
<div class="avue-card__avatar">
|
||||
<img :src="thumbMediaUrl" alt="" />
|
||||
</div>
|
||||
<div class="avue-card__detail">
|
||||
<div class="avue-card__title" style="margin-bottom: unset">{{ title }}</div>
|
||||
<div class="avue-card__info" style="height: unset">{{ description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'WxMusic' })
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
description: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
musicUrl: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
hqMusicUrl: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
thumbMediaUrl: {
|
||||
required: true,
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
musicUrl: props.musicUrl
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scss */
|
||||
@import url('../wx-msg/card.scss');
|
||||
</style>
|
||||
@ -1,3 +0,0 @@
|
||||
import WxNews from './main.vue'
|
||||
|
||||
export default WxNews
|
||||
@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<div class="select-item" v-if="reply.articles && reply.articles.length > 0">
|
||||
<WxNews :articles="reply.articles" />
|
||||
<el-col class="ope-row">
|
||||
<el-button type="danger" circle @click="onDelete">
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
</div>
|
||||
<!-- 选择素材 -->
|
||||
<el-col :span="24" v-if="!reply.content">
|
||||
<el-row style="text-align: center" align="middle">
|
||||
<el-col :span="24">
|
||||
<el-button type="success" @click="showDialog = true">
|
||||
{{ newsType === NewsType.Published ? '选择已发布图文' : '选择草稿箱图文' }}
|
||||
<Icon icon="ep:circle-check" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
<el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close>
|
||||
<WxMaterialSelect
|
||||
type="news"
|
||||
:account-id="reply.accountId"
|
||||
:newsType="newsType"
|
||||
@select-material="selectMaterial"
|
||||
/>
|
||||
</el-dialog>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
|
||||
import { Reply, NewsType } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply
|
||||
newsType: NewsType
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: Reply)
|
||||
}>()
|
||||
const reply = computed<Reply>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
|
||||
const selectMaterial = (item: any) => {
|
||||
showDialog.value = false
|
||||
reply.value.articles = item.content.newsItem
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
reply.value.articles = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-item {
|
||||
width: 280px;
|
||||
padding: 10px;
|
||||
margin: 0 auto 10px;
|
||||
border: 1px solid #eaeaea;
|
||||
|
||||
.ope-row {
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="content" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
modelValue?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: string | null)
|
||||
(e: 'input', v: string | null)
|
||||
}>()
|
||||
|
||||
const content = computed<string | null | undefined>({
|
||||
get: () => props.modelValue,
|
||||
set: (val: string | null) => {
|
||||
emit('update:modelValue', val)
|
||||
emit('input', val)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -1,54 +0,0 @@
|
||||
enum ReplyType {
|
||||
News = 'news',
|
||||
Image = 'image',
|
||||
Voice = 'voice',
|
||||
Video = 'video',
|
||||
Music = 'music',
|
||||
Text = 'text'
|
||||
}
|
||||
|
||||
interface _Reply {
|
||||
accountId: number
|
||||
type: ReplyType
|
||||
name?: string | null
|
||||
content?: string | null
|
||||
mediaId?: string | null
|
||||
url?: string | null
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
thumbMediaId?: string | null
|
||||
thumbMediaUrl?: string | null
|
||||
musicUrl?: string | null
|
||||
hqMusicUrl?: string | null
|
||||
introduction?: string | null
|
||||
articles?: any[]
|
||||
}
|
||||
|
||||
type Reply = _Reply //Partial<_Reply>
|
||||
|
||||
enum NewsType {
|
||||
Published = '1',
|
||||
Draft = '2'
|
||||
}
|
||||
|
||||
/** 利用旧的reply[accountId, type]初始化新的Reply */
|
||||
const createEmptyReply = (old: Reply | Ref<Reply>): Reply => {
|
||||
return {
|
||||
accountId: unref(old).accountId,
|
||||
type: unref(old).type,
|
||||
name: null,
|
||||
content: null,
|
||||
mediaId: null,
|
||||
url: null,
|
||||
title: null,
|
||||
description: null,
|
||||
thumbMediaId: null,
|
||||
thumbMediaUrl: null,
|
||||
musicUrl: null,
|
||||
hqMusicUrl: null,
|
||||
introduction: null,
|
||||
articles: []
|
||||
}
|
||||
}
|
||||
|
||||
export { Reply, NewsType, ReplyType, createEmptyReply }
|
||||
@ -1,7 +0,0 @@
|
||||
import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types'
|
||||
|
||||
import WxReplySelect from './main.vue'
|
||||
|
||||
export type { Reply }
|
||||
export { createEmptyReply, NewsType, ReplyType }
|
||||
export default WxReplySelect
|
||||
@ -1,3 +0,0 @@
|
||||
import WxVideoPlayer from './main.vue'
|
||||
|
||||
export default WxVideoPlayer
|
||||
@ -1,3 +0,0 @@
|
||||
import WxVoicePlayer from './main.vue'
|
||||
|
||||
export default WxVoicePlayer
|
||||
@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div class="waterfall" v-loading="props.loading">
|
||||
<template v-for="item in props.list" :key="item.articleId">
|
||||
<div class="waterfall-item" v-if="item.content && item.content.newsItem">
|
||||
<WxNews :articles="item.content.newsItem" />
|
||||
<!-- 操作按钮 -->
|
||||
<el-row>
|
||||
<el-button
|
||||
type="success"
|
||||
circle
|
||||
@click="emit('publish', item)"
|
||||
v-hasPermi="['mp:free-publish:submit']"
|
||||
>
|
||||
<Icon icon="fa:upload" />
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@click="emit('update', item)"
|
||||
v-hasPermi="['mp:draft:update']"
|
||||
>
|
||||
<Icon icon="ep:edit" />
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
circle
|
||||
@click="emit('delete', item)"
|
||||
v-hasPermi="['mp:draft:delete']"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
|
||||
import { Article } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
list: Article[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'publish', v: Article)
|
||||
(e: 'update', v: Article)
|
||||
(e: 'delete', v: Article)
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 992px) and (width <= 1300px) {
|
||||
.waterfall {
|
||||
column-count: 3;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 768px) and (width <= 991px) {
|
||||
.waterfall {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.waterfall {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,304 +0,0 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-aside width="40%">
|
||||
<div class="select-item">
|
||||
<div v-for="(news, index) in newsList" :key="index">
|
||||
<div
|
||||
class="news-main father"
|
||||
v-if="index === 0"
|
||||
:class="{ activeAddNews: activeNewsIndex === index }"
|
||||
@click="activeNewsIndex = index"
|
||||
>
|
||||
<div class="news-content">
|
||||
<img class="material-img" :src="news.thumbUrl" />
|
||||
<div class="news-content-title">{{ news.title }}</div>
|
||||
</div>
|
||||
<div class="child" v-if="newsList.length > 1">
|
||||
<el-button type="info" circle size="small" @click="() => moveDownNews(index)">
|
||||
<Icon icon="ep:arrow-down-bold" />
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isCreating"
|
||||
type="danger"
|
||||
circle
|
||||
size="small"
|
||||
@click="() => removeNews(index)"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="news-main-item father"
|
||||
v-if="index > 0"
|
||||
:class="{ activeAddNews: activeNewsIndex === index }"
|
||||
@click="activeNewsIndex = index"
|
||||
>
|
||||
<div class="news-content-item">
|
||||
<div class="news-content-item-title">{{ news.title }}</div>
|
||||
<div class="news-content-item-img">
|
||||
<img class="material-img" :src="news.thumbUrl" width="100%" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="child">
|
||||
<el-button
|
||||
v-if="newsList.length > index + 1"
|
||||
circle
|
||||
type="info"
|
||||
size="small"
|
||||
@click="() => moveDownNews(index)"
|
||||
>
|
||||
<Icon icon="ep:arrow-down-bold" />
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="index > 0"
|
||||
type="info"
|
||||
circle
|
||||
size="small"
|
||||
@click="() => moveUpNews(index)"
|
||||
>
|
||||
<Icon icon="ep:arrow-up-bold" />
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isCreating"
|
||||
type="danger"
|
||||
size="small"
|
||||
circle
|
||||
@click="() => removeNews(index)"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-row justify="center" class="ope-row">
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@click="plusNews"
|
||||
v-if="newsList.length < 8 && isCreating"
|
||||
>
|
||||
<Icon icon="ep:plus" />
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<div v-if="newsList.length > 0">
|
||||
<!-- 标题、作者、原文地址 -->
|
||||
<el-row :gutter="20">
|
||||
<el-input v-model="activeNewsItem.title" placeholder="请输入标题(必填)" />
|
||||
<el-input
|
||||
v-model="activeNewsItem.author"
|
||||
placeholder="请输入作者"
|
||||
style="margin-top: 5px"
|
||||
/>
|
||||
<el-input
|
||||
v-model="activeNewsItem.contentSourceUrl"
|
||||
placeholder="请输入原文地址"
|
||||
style="margin-top: 5px"
|
||||
/>
|
||||
</el-row>
|
||||
<!-- 封面和摘要 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<CoverSelect v-model="activeNewsItem" :is-first="activeNewsIndex === 0" />
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<p>摘要:</p>
|
||||
<el-input
|
||||
:rows="8"
|
||||
type="textarea"
|
||||
v-model="activeNewsItem.digest"
|
||||
placeholder="请输入摘要"
|
||||
class="digest"
|
||||
maxlength="120"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!--富文本编辑器组件-->
|
||||
<el-row>
|
||||
<Editor v-model="activeNewsItem.content" :editor-config="editorConfig" />
|
||||
</el-row>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { createEditorConfig } from '../editor-config'
|
||||
import CoverSelect from './CoverSelect.vue'
|
||||
import { type NewsItem, createEmptyNewsItem } from './types'
|
||||
|
||||
defineOptions({ name: 'NewsForm' })
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const props = defineProps<{
|
||||
isCreating: boolean
|
||||
modelValue: NewsItem[] | null
|
||||
}>()
|
||||
|
||||
const accountId = inject<number>('accountId')
|
||||
|
||||
// ========== 文件上传 ==========
|
||||
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传永久素材的地址
|
||||
const editorConfig = createEditorConfig(UPLOAD_URL, accountId)
|
||||
|
||||
// v-model=newsList
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: NewsItem[])
|
||||
}>()
|
||||
const newsList = computed<NewsItem[]>({
|
||||
get() {
|
||||
return props.modelValue === null ? [createEmptyNewsItem()] : props.modelValue
|
||||
},
|
||||
set(val) {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
})
|
||||
|
||||
const activeNewsIndex = ref(0)
|
||||
const activeNewsItem = computed<NewsItem>(() => newsList.value[activeNewsIndex.value])
|
||||
|
||||
// 将图文向下移动
|
||||
const moveDownNews = (index: number) => {
|
||||
const temp = newsList.value[index]
|
||||
newsList.value[index] = newsList.value[index + 1]
|
||||
newsList.value[index + 1] = temp
|
||||
activeNewsIndex.value = index + 1
|
||||
}
|
||||
|
||||
// 将图文向上移动
|
||||
const moveUpNews = (index: number) => {
|
||||
const temp = newsList.value[index]
|
||||
newsList.value[index] = newsList.value[index - 1]
|
||||
newsList.value[index - 1] = temp
|
||||
activeNewsIndex.value = index - 1
|
||||
}
|
||||
|
||||
// 删除指定 index 的图文
|
||||
const removeNews = async (index: number) => {
|
||||
try {
|
||||
await message.confirm('确定删除该图文吗?')
|
||||
newsList.value.splice(index, 1)
|
||||
if (activeNewsIndex.value === index) {
|
||||
activeNewsIndex.value = 0
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 添加一个图文
|
||||
const plusNews = () => {
|
||||
newsList.value.push(createEmptyNewsItem())
|
||||
activeNewsIndex.value = newsList.value.length - 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ope-row {
|
||||
padding-top: 5px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.el-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.digest {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* 新增图文 */
|
||||
.news-main {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
margin: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background-color: #acadae;
|
||||
}
|
||||
|
||||
.news-content-title {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
width: 98%;
|
||||
height: 25px;
|
||||
padding: 1%;
|
||||
overflow: hidden;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
background-color: black;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.news-main-item {
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
margin: auto;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.news-content-item {
|
||||
position: relative;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.news-content-item-title {
|
||||
display: inline-block;
|
||||
width: 70%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.news-content-item-img {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
background-color: #acadae;
|
||||
}
|
||||
|
||||
.select-item {
|
||||
width: 60%;
|
||||
padding: 10px;
|
||||
margin: 0 auto 10px;
|
||||
border: 1px solid #eaeaea;
|
||||
|
||||
.activeAddNews {
|
||||
border: 5px solid #2bb673;
|
||||
}
|
||||
}
|
||||
|
||||
.father .child {
|
||||
position: relative;
|
||||
bottom: 25px;
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.father:hover .child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.material-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +0,0 @@
|
||||
import type { Article, NewsItem, NewsItemList } from './types'
|
||||
import { createEmptyNewsItem } from './types'
|
||||
import DraftTable from './DraftTable.vue'
|
||||
import NewsForm from './NewsForm.vue'
|
||||
|
||||
export { DraftTable, NewsForm, createEmptyNewsItem }
|
||||
export type { Article, NewsItem, NewsItemList }
|
||||
@ -1,40 +0,0 @@
|
||||
interface NewsItem {
|
||||
title: string
|
||||
thumbMediaId: string
|
||||
author: string
|
||||
digest: string
|
||||
showCoverPic: string
|
||||
content: string
|
||||
contentSourceUrl: string
|
||||
needOpenComment: string
|
||||
onlyFansCanComment: string
|
||||
thumbUrl: string
|
||||
}
|
||||
|
||||
interface NewsItemList {
|
||||
newsItem: NewsItem[]
|
||||
}
|
||||
|
||||
interface Article {
|
||||
mediaId: string
|
||||
content: NewsItemList
|
||||
updateTime: number
|
||||
}
|
||||
|
||||
const createEmptyNewsItem = (): NewsItem => {
|
||||
return {
|
||||
title: '',
|
||||
thumbMediaId: '',
|
||||
author: '',
|
||||
digest: '',
|
||||
showCoverPic: '',
|
||||
content: '',
|
||||
contentSourceUrl: '',
|
||||
needOpenComment: '',
|
||||
onlyFansCanComment: '',
|
||||
thumbUrl: ''
|
||||
}
|
||||
}
|
||||
|
||||
export type { Article, NewsItem, NewsItemList }
|
||||
export { createEmptyNewsItem }
|
||||
@ -1,336 +0,0 @@
|
||||
<template>
|
||||
<doc-alert title="公众号图文" url="https://doc.iocoder.cn/mp/article/" />
|
||||
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="公众号" prop="accountId">
|
||||
<WxAccountSelect @change="onAccountChanged" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<div class="waterfall" v-loading="loading">
|
||||
<div
|
||||
class="waterfall-item"
|
||||
v-show="item.content && item.content.newsItem"
|
||||
v-for="item in list"
|
||||
:key="item.articleId"
|
||||
>
|
||||
<wx-news :articles="item.content.newsItem" />
|
||||
<el-row justify="center" class="ope-row">
|
||||
<el-button
|
||||
type="danger"
|
||||
circle
|
||||
@click="handleDelete(item)"
|
||||
v-hasPermi="['mp:free-publish:delete']"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as FreePublishApi from '@/api/mp/freePublish'
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
import WxAccountSelect from '@/views/mp/components/wx-account-select'
|
||||
|
||||
defineOptions({ name: 'MpFreePublish' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<any[]>([]) // 列表的数据
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: -1
|
||||
})
|
||||
|
||||
/** 侦听公众号变化 **/
|
||||
const onAccountChanged = (id: number) => {
|
||||
queryParams.accountId = id
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const data = await FreePublishApi.getFreePublishPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (item: any) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm('删除后用户将无法访问此页面,确定删除?')
|
||||
// 发起删除
|
||||
await FreePublishApi.deleteFreePublish(queryParams.accountId, item.articleId)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@media (width >= 992px) and (width <= 1300px) {
|
||||
.waterfall {
|
||||
column-count: 3;
|
||||
}
|
||||
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 768px) and (width <= 991px) {
|
||||
.waterfall {
|
||||
column-count: 2;
|
||||
}
|
||||
|
||||
p {
|
||||
color: orange;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.waterfall {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ope-row {
|
||||
padding-top: 5px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* 新增图文 */
|
||||
.left {
|
||||
display: inline-block;
|
||||
width: 35%;
|
||||
margin-top: 200px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: inline-block;
|
||||
width: 60%;
|
||||
margin-top: -40px;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
display: inline-block;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.avatar-uploader .el-upload {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: unset !important;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.avatar-uploader .el-upload:hover {
|
||||
border-color: #165dff;
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
font-size: 28px;
|
||||
line-height: 120px;
|
||||
color: #8c939d;
|
||||
text-align: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 230px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.avatar1 {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.digest {
|
||||
display: inline-block;
|
||||
width: 60%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* 新增图文 */
|
||||
|
||||
/* 瀑布流样式 */
|
||||
.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;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
/* 瀑布流样式 */
|
||||
.news-main {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
margin: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background-color: #acadae;
|
||||
}
|
||||
|
||||
.news-content-title {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
width: 98%;
|
||||
height: 25px;
|
||||
padding: 1%;
|
||||
overflow: hidden;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
background-color: black;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.news-main-item {
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
margin: auto;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.news-content-item {
|
||||
position: relative;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.news-content-item-title {
|
||||
display: inline-block;
|
||||
width: 70%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.news-content-item-img {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
background-color: #acadae;
|
||||
}
|
||||
|
||||
.input-tt {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.activeAddNews {
|
||||
border: 5px solid #2bb673;
|
||||
}
|
||||
|
||||
.news-main-plus {
|
||||
width: 280px;
|
||||
height: 50px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-plus {
|
||||
margin: 10px;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.select-item {
|
||||
width: 60%;
|
||||
padding: 10px;
|
||||
margin: 0 auto 10px;
|
||||
border: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.father .child {
|
||||
position: relative;
|
||||
bottom: 25px;
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.father:hover .child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-div {
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.thumb-but {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.material-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -1,50 +0,0 @@
|
||||
import type { UploadRawFile } from 'element-plus'
|
||||
|
||||
const message = useMessage() // 消息
|
||||
|
||||
enum UploadType {
|
||||
Image = 'image',
|
||||
Voice = 'voice',
|
||||
Video = 'video'
|
||||
}
|
||||
|
||||
const useBeforeUpload = (type: UploadType, maxSizeMB: number) => {
|
||||
const fn = (rawFile: UploadRawFile): boolean => {
|
||||
let allowTypes: string[] = []
|
||||
let name = ''
|
||||
|
||||
switch (type) {
|
||||
case UploadType.Image:
|
||||
allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
|
||||
maxSizeMB = 2
|
||||
name = '图片'
|
||||
break
|
||||
case UploadType.Voice:
|
||||
allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr']
|
||||
maxSizeMB = 2
|
||||
name = '语音'
|
||||
break
|
||||
case UploadType.Video:
|
||||
allowTypes = ['video/mp4']
|
||||
maxSizeMB = 10
|
||||
name = '视频'
|
||||
break
|
||||
}
|
||||
// 格式不正确
|
||||
if (!allowTypes.includes(rawFile.type)) {
|
||||
message.error(`上传${name}格式不对!`)
|
||||
return false
|
||||
}
|
||||
// 大小不正确
|
||||
if (rawFile.size / 1024 / 1024 > maxSizeMB) {
|
||||
message.error(`上传${name}大小不能超过${maxSizeMB}M!`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return fn
|
||||
}
|
||||
|
||||
export { UploadType, useBeforeUpload }
|
||||
@ -1,77 +0,0 @@
|
||||
<template>
|
||||
<el-upload
|
||||
:action="UPLOAD_URL"
|
||||
:headers="HEADERS"
|
||||
multiple
|
||||
:limit="1"
|
||||
:file-list="fileList"
|
||||
:data="uploadData"
|
||||
:on-error="onUploadError"
|
||||
:before-upload="onBeforeUpload"
|
||||
:on-success="onUploadSuccess"
|
||||
>
|
||||
<el-button type="primary" plain> 点击上传 </el-button>
|
||||
<template #tip>
|
||||
<span class="el-upload__tip" style="margin-left: 5px">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { UploadProps, UploadUserFile } from 'element-plus'
|
||||
import {
|
||||
HEADERS,
|
||||
UPLOAD_URL,
|
||||
UploadData,
|
||||
UploadType,
|
||||
beforeImageUpload,
|
||||
beforeVoiceUpload
|
||||
} from './upload'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const props = defineProps<{ type: UploadType }>()
|
||||
|
||||
const accountId = inject<number>('accountId')
|
||||
|
||||
const fileList = ref<UploadUserFile[]>([])
|
||||
const emit = defineEmits<{
|
||||
(e: 'uploaded', v: void)
|
||||
}>()
|
||||
|
||||
const uploadData: UploadData = reactive({
|
||||
type: UploadType.Image,
|
||||
title: '',
|
||||
introduction: '',
|
||||
accountId: accountId!
|
||||
})
|
||||
|
||||
/** 上传前检查 */
|
||||
const onBeforeUpload = props.type === UploadType.Image ? beforeImageUpload : beforeVoiceUpload
|
||||
|
||||
/** 上传成功处理 */
|
||||
const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
|
||||
if (res.code !== 0) {
|
||||
message.alertError('上传出错:' + res.msg)
|
||||
return false
|
||||
}
|
||||
|
||||
// 清空上传时的各种数据
|
||||
fileList.value = []
|
||||
uploadData.title = ''
|
||||
uploadData.introduction = ''
|
||||
|
||||
message.notifySuccess('上传成功')
|
||||
emit('uploaded')
|
||||
}
|
||||
|
||||
/** 上传失败处理 */
|
||||
const onUploadError = (err: Error) => message.error('上传失败: ' + err.message)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-upload__tip {
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<el-table :data="props.list" stripe border v-loading="props.loading" style="margin-top: 10px">
|
||||
<el-table-column label="编号" align="center" prop="mediaId" />
|
||||
<el-table-column label="文件名" align="center" prop="name" />
|
||||
<el-table-column label="标题" align="center" prop="title" />
|
||||
<el-table-column label="介绍" align="center" prop="introduction" />
|
||||
<el-table-column label="视频" align="center">
|
||||
<template #default="scope">
|
||||
<WxVideoPlayer v-if="scope.row.url" :url="scope.row.url" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="上传时间"
|
||||
align="center"
|
||||
:formatter="dateFormatter"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleDownload(scope.row.url)">
|
||||
<Icon icon="ep:download" />下载
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="emit('delete', scope.row.id)"
|
||||
v-hasPermi="['mp:material:delete']"
|
||||
>
|
||||
<Icon icon="ep:delete" />删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
|
||||
const props = defineProps<{
|
||||
list: any[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'delete', v: number)
|
||||
(e: 'download', v: string)
|
||||
}>()
|
||||
|
||||
// 下载文件
|
||||
const handleDownload = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<el-table :data="props.list" stripe border v-loading="props.loading" style="margin-top: 10px">
|
||||
<el-table-column label="编号" align="center" prop="mediaId" />
|
||||
<el-table-column label="文件名" align="center" prop="name" />
|
||||
<el-table-column label="语音" align="center">
|
||||
<template #default="scope">
|
||||
<WxVoicePlayer v-if="scope.row.url" :url="scope.row.url" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="上传时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="emit('delete', scope.row.id)">
|
||||
<Icon icon="ep:download" />下载
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="emit('delete', scope.row.id)"
|
||||
v-hasPermi="['mp:material:delete']"
|
||||
>
|
||||
<Icon icon="ep:delete" />删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
|
||||
const props = defineProps<{
|
||||
list: any[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'delete', v: number)
|
||||
}>()
|
||||
</script>
|
||||
@ -1,32 +0,0 @@
|
||||
import type { UploadProps, UploadRawFile } from 'element-plus'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
|
||||
|
||||
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头
|
||||
const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址
|
||||
|
||||
interface UploadData {
|
||||
type: UploadType
|
||||
title: string
|
||||
introduction: string
|
||||
accountId: number
|
||||
}
|
||||
|
||||
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
|
||||
useBeforeUpload(UploadType.Image, 2)(rawFile)
|
||||
|
||||
const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
|
||||
useBeforeUpload(UploadType.Voice, 2)(rawFile)
|
||||
|
||||
const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
|
||||
useBeforeUpload(UploadType.Video, 10)(rawFile)
|
||||
|
||||
export {
|
||||
HEADERS,
|
||||
UPLOAD_URL,
|
||||
UploadType,
|
||||
UploadData,
|
||||
beforeImageUpload,
|
||||
beforeVoiceUpload,
|
||||
beforeVideoUpload
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@ -1,42 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
value: 'view',
|
||||
label: '跳转网页'
|
||||
},
|
||||
{
|
||||
value: 'miniprogram',
|
||||
label: '跳转小程序'
|
||||
},
|
||||
{
|
||||
value: 'click',
|
||||
label: '点击回复'
|
||||
},
|
||||
{
|
||||
value: 'article_view_limited',
|
||||
label: '跳转图文消息'
|
||||
},
|
||||
{
|
||||
value: 'scancode_push',
|
||||
label: '扫码直接返回结果'
|
||||
},
|
||||
{
|
||||
value: 'scancode_waitmsg',
|
||||
label: '扫码回复'
|
||||
},
|
||||
{
|
||||
value: 'pic_sysphoto',
|
||||
label: '系统拍照发图'
|
||||
},
|
||||
{
|
||||
value: 'pic_photo_or_album',
|
||||
label: '拍照或者相册'
|
||||
},
|
||||
{
|
||||
value: 'pic_weixin',
|
||||
label: '微信相册'
|
||||
},
|
||||
{
|
||||
value: 'location_select',
|
||||
label: '选择地理位置'
|
||||
}
|
||||
]
|
||||
@ -1,73 +0,0 @@
|
||||
export interface Replay {
|
||||
title: string
|
||||
description: string
|
||||
picUrl: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export type MenuType =
|
||||
| ''
|
||||
| 'click'
|
||||
| 'view'
|
||||
| 'scancode_waitmsg'
|
||||
| 'scancode_push'
|
||||
| 'pic_sysphoto'
|
||||
| 'pic_photo_or_album'
|
||||
| 'pic_weixin'
|
||||
| 'location_select'
|
||||
| 'article_view_limited'
|
||||
|
||||
interface _RawMenu {
|
||||
// db
|
||||
id: number
|
||||
parentId: number
|
||||
accountId: number
|
||||
appId: string
|
||||
createTime: number
|
||||
|
||||
// mp-native
|
||||
name: string
|
||||
menuKey: string
|
||||
type: MenuType
|
||||
url: string
|
||||
miniProgramAppId: string
|
||||
miniProgramPagePath: string
|
||||
articleId: string
|
||||
replyMessageType: string
|
||||
replyContent: string
|
||||
replyMediaId: string
|
||||
replyMediaUrl: string
|
||||
replyThumbMediaId: string
|
||||
replyThumbMediaUrl: string
|
||||
replyTitle: string
|
||||
replyDescription: string
|
||||
replyArticles: Replay
|
||||
replyMusicUrl: string
|
||||
replyHqMusicUrl: string
|
||||
}
|
||||
|
||||
export type RawMenu = Partial<_RawMenu>
|
||||
|
||||
interface _Reply {
|
||||
type: string
|
||||
accountId: number
|
||||
content: string
|
||||
mediaId: string
|
||||
url: string
|
||||
thumbMediaId: string
|
||||
thumbMediaUrl: string
|
||||
title: string
|
||||
description: string
|
||||
articles: null | Replay[]
|
||||
musicUrl: string
|
||||
hqMusicUrl: string
|
||||
}
|
||||
|
||||
export type Reply = Partial<_Reply>
|
||||
|
||||
interface _Menu extends RawMenu {
|
||||
children: _Menu[]
|
||||
reply: Reply
|
||||
}
|
||||
|
||||
export type Menu = Partial<_Menu>
|
||||
Loading…
Reference in New Issue