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