mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2025-12-30 02:22:25 +00:00
fix: todo 处理
This commit is contained in:
@@ -118,7 +118,13 @@ async function handleDelete(id: number) {
|
||||
</span>
|
||||
</template>
|
||||
<!-- 列表 -->
|
||||
<ImageTable :list="list" :loading="loading" @delete="handleDelete">
|
||||
<ImageTable
|
||||
:key="`image-${type}`"
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
@refresh="getList"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<UploadFile
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
@@ -149,7 +155,13 @@ async function handleDelete(id: number) {
|
||||
</span>
|
||||
</template>
|
||||
<!-- 列表 -->
|
||||
<VoiceTable :list="list" :loading="loading" @delete="handleDelete">
|
||||
<VoiceTable
|
||||
:key="`voice-${type}`"
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
@refresh="getList"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<UploadFile
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
@@ -180,7 +192,13 @@ async function handleDelete(id: number) {
|
||||
</span>
|
||||
</template>
|
||||
<!-- 列表 -->
|
||||
<VideoTable :list="list" :loading="loading" @delete="handleDelete">
|
||||
<VideoTable
|
||||
:key="`video-${type}`"
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
@refresh="getList"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
|
||||
@@ -14,6 +14,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const columns = useImageGridColumns();
|
||||
@@ -35,6 +36,21 @@ const [Grid, gridApi] = useVbenVxeGrid<MpMaterialApi.Material>({
|
||||
refresh: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
// 数据由父组件管理,触发刷新事件后返回当前数据
|
||||
emit('refresh');
|
||||
// 返回当前数据,避免覆盖
|
||||
return {
|
||||
list: Array.isArray(props.list) ? props.list : [],
|
||||
total: props.list?.length || 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
autoLoad: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,7 +69,7 @@ watch(
|
||||
await nextTick();
|
||||
updateGridData(data);
|
||||
},
|
||||
{ flush: 'post' },
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
@@ -19,6 +19,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const columns = useVideoGridColumns();
|
||||
@@ -39,20 +40,40 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
// 数据由父组件管理,触发刷新事件后返回当前数据
|
||||
emit('refresh');
|
||||
// 返回当前数据,避免覆盖
|
||||
return {
|
||||
list: Array.isArray(props.list) ? props.list : [],
|
||||
total: props.list?.length || 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
autoLoad: false,
|
||||
},
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
function updateGridData(data: MpMaterialApi.Material[]) {
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(list: MpMaterialApi.Material[]) => {
|
||||
async (list: MpMaterialApi.Material[]) => {
|
||||
const data = Array.isArray(list) ? list : [];
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
await nextTick();
|
||||
updateGridData(data);
|
||||
},
|
||||
{ immediate: true },
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
@@ -19,6 +19,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const columns = useVoiceGridColumns();
|
||||
@@ -39,20 +40,40 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
// 数据由父组件管理,触发刷新事件后返回当前数据
|
||||
emit('refresh');
|
||||
// 返回当前数据,避免覆盖
|
||||
return {
|
||||
list: Array.isArray(props.list) ? props.list : [],
|
||||
total: props.list?.length || 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
autoLoad: false,
|
||||
},
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
function updateGridData(data: MpMaterialApi.Material[]) {
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(list: MpMaterialApi.Material[]) => {
|
||||
async (list: MpMaterialApi.Material[]) => {
|
||||
const data = Array.isArray(list) ? list : [];
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
await nextTick();
|
||||
updateGridData(data);
|
||||
},
|
||||
{ immediate: true },
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
|
||||
91
apps/web-antd/src/views/mp/message/data.ts
Normal file
91
apps/web-antd/src/views/mp/message/data.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMessageApi } from '#/api/mp/message';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'accountId',
|
||||
label: '公众号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '消息类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择消息类型',
|
||||
options: getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'openid',
|
||||
label: '用户标识',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户标识',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<MpMessageApi.Message>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发送时间',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
slots: { default: 'createTime' },
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '消息类型',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'sendFrom',
|
||||
title: '发送方',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
slots: { default: 'sendFrom' },
|
||||
},
|
||||
{
|
||||
field: 'openid',
|
||||
title: '用户标识',
|
||||
width: 300,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
title: '内容',
|
||||
align: 'left',
|
||||
minWidth: 320,
|
||||
slots: { default: 'content' },
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,185 +1,228 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMessageApi } from '#/api/mp/message';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { DICT_TYPE, MpMsgType } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { MpMsgType as MsgType } from '@vben/constants';
|
||||
import { formatDate2 } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Select,
|
||||
} from 'ant-design-vue';
|
||||
import { Image, Modal, Tag } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getMessagePage } from '#/api/mp/message';
|
||||
import { WxAccountSelect, WxMsg } from '#/views/mp/components';
|
||||
import {
|
||||
WxAccountSelect,
|
||||
WxLocation,
|
||||
WxMsg,
|
||||
WxMusic,
|
||||
WxNews,
|
||||
WxVideoPlayer,
|
||||
WxVoicePlayer,
|
||||
} from '#/views/mp/components';
|
||||
|
||||
import MessageTable from './message-table.vue';
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'MpMessage' });
|
||||
|
||||
const loading = ref(false);
|
||||
const total = ref(0); // 数据的总页数
|
||||
const list = ref<any[]>([]); // 当前页的列表数据
|
||||
|
||||
const queryParams = reactive<{
|
||||
accountId: number;
|
||||
createTime: [Dayjs, Dayjs] | undefined;
|
||||
openid: string;
|
||||
pageNo: number;
|
||||
pageSize: number;
|
||||
type: string;
|
||||
}>({
|
||||
accountId: -1,
|
||||
createTime: undefined,
|
||||
openid: '',
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: MpMsgType.Text,
|
||||
}); // 搜索参数
|
||||
|
||||
const queryFormRef = ref(); // 搜索的表单
|
||||
|
||||
// 消息对话框
|
||||
const messageBoxVisible = ref(false);
|
||||
const messageBoxUserId = ref(0);
|
||||
|
||||
/** 侦听 accountId */
|
||||
function onAccountChanged(id: number) {
|
||||
queryParams.accountId = id;
|
||||
queryParams.pageNo = 1;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = await getMessagePage(queryParams);
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
async function resetQuery() {
|
||||
// 暂存 accountId,并在 reset 后恢复
|
||||
const accountId = queryParams.accountId;
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.accountId = accountId;
|
||||
handleQuery();
|
||||
/** 公众号变化时查询数据 */
|
||||
function handleAccountChange(accountId: number) {
|
||||
gridApi.formApi.setValues({ accountId });
|
||||
gridApi.formApi.submitForm();
|
||||
}
|
||||
|
||||
/** 打开消息发送窗口 */
|
||||
async function handleSend(userId: number) {
|
||||
function handleSend(userId: number) {
|
||||
messageBoxUserId.value = userId;
|
||||
messageBoxVisible.value = true;
|
||||
}
|
||||
|
||||
/** 分页改变事件 */
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
queryParams.pageNo = page;
|
||||
queryParams.pageSize = pageSize;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 显示总条数 */
|
||||
function showTotal(total: number) {
|
||||
return `共 ${total} 条`;
|
||||
}
|
||||
// TODO @dylan:是不是应该都用 Grid 哈:1)message-table 大部分合并到 index.vue;2)message-table 的 schema 放到 data.ts 里;
|
||||
const [Grid, gridApi] = useVbenVxeGrid<MpMessageApi.Message>({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getMessagePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
autoLoad: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
} as VxeTableGridOptions<MpMessageApi.Message>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height class="flex flex-col">
|
||||
<!-- 搜索工作栏 -->
|
||||
<div class="mb-4 rounded-lg bg-background p-4">
|
||||
<Form
|
||||
ref="queryFormRef"
|
||||
:model="queryParams"
|
||||
layout="inline"
|
||||
class="search-form"
|
||||
>
|
||||
<FormItem label="公众号" name="accountId">
|
||||
<WxAccountSelect @change="onAccountChanged" />
|
||||
</FormItem>
|
||||
<FormItem label="消息类型" name="type">
|
||||
<Select
|
||||
v-model:value="queryParams.type"
|
||||
placeholder="请选择消息类型"
|
||||
class="!w-[240px]"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="用户标识" name="openid">
|
||||
<Input
|
||||
v-model:value="queryParams.openid"
|
||||
placeholder="请输入用户标识"
|
||||
allow-clear
|
||||
class="!w-[240px]"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="创建时间" name="createTime">
|
||||
<DatePicker.RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
:show-time="true"
|
||||
class="!w-[240px]"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:magnify" />
|
||||
</template>
|
||||
搜索
|
||||
</Button>
|
||||
<Button class="ml-2" @click="resetQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #form-accountId>
|
||||
<WxAccountSelect @change="handleAccountChange" />
|
||||
</template>
|
||||
<template #createTime="{ row }">
|
||||
{{ row.createTime ? formatDate2(row.createTime) : '' }}
|
||||
</template>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="flex-1 rounded-lg bg-background p-4">
|
||||
<MessageTable :list="list" :loading="loading" @send="handleSend" />
|
||||
<div v-show="total > 0" class="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
show-quick-jumper
|
||||
:show-total="showTotal"
|
||||
@change="handlePageChange"
|
||||
<template #sendFrom="{ row }">
|
||||
<Tag v-if="row.sendFrom === 1" color="success">粉丝</Tag>
|
||||
<Tag v-else>公众号</Tag>
|
||||
</template>
|
||||
|
||||
<template #content="{ row }">
|
||||
<div
|
||||
v-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'subscribe'
|
||||
"
|
||||
>
|
||||
<Tag color="success">关注</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'unsubscribe'
|
||||
"
|
||||
>
|
||||
<Tag color="error">取消关注</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'CLICK'
|
||||
"
|
||||
>
|
||||
<Tag>点击菜单</Tag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event && row.event === 'VIEW'">
|
||||
<Tag>点击菜单链接</Tag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'scancode_waitmsg'
|
||||
"
|
||||
>
|
||||
<Tag>扫码结果</Tag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'scancode_push'
|
||||
"
|
||||
>
|
||||
<Tag>扫码结果</Tag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Event && row.event === 'pic_sysphoto'"
|
||||
>
|
||||
<Tag>系统拍照发图</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'pic_photo_or_album'
|
||||
"
|
||||
>
|
||||
<Tag>拍照或者相册</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Event && row.event === 'pic_weixin'"
|
||||
>
|
||||
<Tag>微信相册</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'location_select'
|
||||
"
|
||||
>
|
||||
<Tag>选择地理位置</Tag>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event">
|
||||
<Tag color="error">未知事件类型</Tag>
|
||||
</div>
|
||||
|
||||
<div v-else-if="row.type === MsgType.Text">{{ row.content }}</div>
|
||||
<div v-else-if="row.type === MsgType.Voice">
|
||||
<WxVoicePlayer
|
||||
:url="row.mediaUrl || ''"
|
||||
:content="row.recognition || ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Image">
|
||||
<a :href="row.mediaUrl" target="_blank">
|
||||
<Image :src="row.mediaUrl" :width="100" :preview="false" />
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Video || row.type === 'shortvideo'"
|
||||
>
|
||||
<WxVideoPlayer :url="row.mediaUrl || ''" class="mt-2" />
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Link">
|
||||
<Tag>链接</Tag>
|
||||
:
|
||||
<a :href="row.url" target="_blank">{{ row.title }}</a>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Location">
|
||||
<WxLocation
|
||||
:label="row.label || ''"
|
||||
:location-y="row.locationY || 0"
|
||||
:location-x="row.locationX || 0"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Music">
|
||||
<WxMusic
|
||||
:title="row.title"
|
||||
:description="row.description"
|
||||
:thumb-media-url="row.thumbMediaUrl || ''"
|
||||
:music-url="row.musicUrl"
|
||||
:hq-music-url="row.hqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.News">
|
||||
<WxNews :articles="row.articles" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<Tag color="error">未知消息类型</Tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '消息',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: () => handleSend(row.userId || 0),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 发送消息的弹窗 -->
|
||||
<Modal
|
||||
@@ -193,9 +236,3 @@ function showTotal(total: number) {
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-form :deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMessageApi } from '#/api/mp/message';
|
||||
|
||||
import { onMounted, watch } from 'vue';
|
||||
|
||||
import { MpMsgType as MsgType } from '@vben/constants';
|
||||
import { formatDate2 } from '@vben/utils';
|
||||
|
||||
import { Button, Image, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
WxLocation,
|
||||
WxMusic,
|
||||
WxNews,
|
||||
WxVideoPlayer,
|
||||
WxVoicePlayer,
|
||||
} from '#/views/mp/components';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list?: MpMessageApi.Message[];
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
list() {
|
||||
return [];
|
||||
},
|
||||
loading: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'send', userId: number): void;
|
||||
}>();
|
||||
|
||||
const columns: VxeTableGridOptions<MpMessageApi.Message>['columns'] = [
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发送时间',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
slots: { default: 'createTime' },
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '消息类型',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'sendFrom',
|
||||
title: '发送方',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
slots: { default: 'sendFrom' },
|
||||
},
|
||||
{
|
||||
field: 'openid',
|
||||
title: '用户标识',
|
||||
width: 300,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
title: '内容',
|
||||
align: 'left',
|
||||
minWidth: 320,
|
||||
slots: { default: 'content' },
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid<MpMessageApi.Message>({
|
||||
gridOptions: {
|
||||
border: true,
|
||||
columns,
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
});
|
||||
|
||||
function normalizeList(list?: MpMessageApi.Message[]) {
|
||||
return Array.isArray(list) ? list : [];
|
||||
}
|
||||
|
||||
function updateGridData(data: MpMessageApi.Message[]) {
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(list) => {
|
||||
updateGridData(normalizeList(list));
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(loading) => {
|
||||
gridApi.setLoading(loading);
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
updateGridData(normalizeList(props.list));
|
||||
gridApi.setLoading(props.loading);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #createTime="{ row }">
|
||||
{{ row.createTime ? formatDate2(row.createTime) : '' }}
|
||||
</template>
|
||||
|
||||
<template #sendFrom="{ row }">
|
||||
<Tag v-if="row.sendFrom === 1" color="success">粉丝</Tag>
|
||||
<Tag v-else color="default">公众号</Tag>
|
||||
</template>
|
||||
|
||||
<template #content="{ row }">
|
||||
<div
|
||||
v-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'subscribe'
|
||||
"
|
||||
>
|
||||
<Tag color="success">关注</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'unsubscribe'
|
||||
"
|
||||
>
|
||||
<Tag color="error">取消关注</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'CLICK'
|
||||
"
|
||||
>
|
||||
<Tag>点击菜单</Tag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event && row.event === 'VIEW'">
|
||||
<Tag>点击菜单链接</Tag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'scancode_waitmsg'
|
||||
"
|
||||
>
|
||||
<Tag>扫码结果</Tag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Event && row.event === 'scancode_push'"
|
||||
>
|
||||
<Tag>扫码结果</Tag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Event && row.event === 'pic_sysphoto'"
|
||||
>
|
||||
<Tag>系统拍照发图</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'pic_photo_or_album'
|
||||
"
|
||||
>
|
||||
<Tag>拍照或者相册</Tag>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event && row.event === 'pic_weixin'">
|
||||
<Tag>微信相册</Tag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'location_select'
|
||||
"
|
||||
>
|
||||
<Tag>选择地理位置</Tag>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event">
|
||||
<Tag color="error">未知事件类型</Tag>
|
||||
</div>
|
||||
|
||||
<div v-else-if="row.type === MsgType.Text">{{ row.content }}</div>
|
||||
<div v-else-if="row.type === MsgType.Voice">
|
||||
<WxVoicePlayer
|
||||
:url="row.mediaUrl || ''"
|
||||
:content="row.recognition || ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Image">
|
||||
<a :href="row.mediaUrl" target="_blank">
|
||||
<Image :src="row.mediaUrl" :width="100" :preview="false" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Video || row.type === 'shortvideo'">
|
||||
<WxVideoPlayer :url="row.mediaUrl || ''" class="mt-2" />
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Link">
|
||||
<Tag>链接</Tag>
|
||||
:
|
||||
<a :href="row.url" target="_blank">{{ row.title }}</a>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Location">
|
||||
<WxLocation
|
||||
:label="row.label || ''"
|
||||
:location-y="row.locationY || 0"
|
||||
:location-x="row.locationX || 0"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Music">
|
||||
<WxMusic
|
||||
:title="row.title"
|
||||
:description="row.description"
|
||||
:thumb-media-url="row.thumbMediaUrl || ''"
|
||||
:music-url="row.musicUrl"
|
||||
:hq-music-url="row.hqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.News">
|
||||
<WxNews :articles="row.articles" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<Tag color="error">未知消息类型</Tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<Button type="link" @click="emit('send', row.userId || 0)"> 消息 </Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -142,7 +142,13 @@ function handleSizeChange(pageSize: number) {
|
||||
</span>
|
||||
</template>
|
||||
<!-- 列表 -->
|
||||
<ImageTable :list="list" :loading="loading" @delete="handleDelete">
|
||||
<ImageTable
|
||||
:key="`image-${type}`"
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
@refresh="getList"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<UploadFile
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
@@ -174,7 +180,13 @@ function handleSizeChange(pageSize: number) {
|
||||
</span>
|
||||
</template>
|
||||
<!-- 列表 -->
|
||||
<VoiceTable :list="list" :loading="loading" @delete="handleDelete">
|
||||
<VoiceTable
|
||||
:key="`voice-${type}`"
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
@refresh="getList"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<UploadFile
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
@@ -206,7 +218,13 @@ function handleSizeChange(pageSize: number) {
|
||||
</span>
|
||||
</template>
|
||||
<!-- 列表 -->
|
||||
<VideoTable :list="list" :loading="loading" @delete="handleDelete">
|
||||
<VideoTable
|
||||
:key="`video-${type}`"
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
@refresh="getList"
|
||||
>
|
||||
<template #toolbar-tools>
|
||||
<ElButton
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
@@ -218,7 +236,7 @@ function handleSizeChange(pageSize: number) {
|
||||
</template>
|
||||
</VideoTable>
|
||||
<!-- 新建视频的弹窗 -->
|
||||
<UploadVideo v-model="showCreateVideo" @uploaded="getList" />
|
||||
<UploadVideo v-model:open="showCreateVideo" @uploaded="getList" />
|
||||
<!-- 分页组件 -->
|
||||
<div class="mt-4 flex justify-end">
|
||||
<ElPagination
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
|
||||
const props = defineProps<{ type: UploadType }>();
|
||||
|
||||
// TODO @dylan:是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
|
||||
const emit = defineEmits<{
|
||||
uploaded: [v: void];
|
||||
}>();
|
||||
@@ -60,8 +59,11 @@ const customRequest: UploadProps['httpRequest'] = async function (options) {
|
||||
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(`上传出错:${res.msg}`);
|
||||
// TODO @dylan:这里有个 linter 错误。
|
||||
onError?.(new Error(res.msg));
|
||||
const error = new Error(res.msg) as any;
|
||||
error.status = 200;
|
||||
error.method = 'POST';
|
||||
error.url = UPLOAD_URL;
|
||||
onError?.(error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,18 +20,17 @@ import {
|
||||
|
||||
import { beforeVideoUpload, HEADERS, UPLOAD_URL, UploadType } from './upload';
|
||||
|
||||
// TODO @dylan:是不是要和 antd 的 props 定义相同哈?这样后续两侧维护方便点
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: boolean;
|
||||
open?: boolean;
|
||||
}>(),
|
||||
{
|
||||
modelValue: false,
|
||||
open: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [v: boolean];
|
||||
'update:open': [v: boolean];
|
||||
uploaded: [v: void];
|
||||
}>();
|
||||
|
||||
@@ -45,7 +44,7 @@ const uploadRules = {
|
||||
};
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:modelValue', false);
|
||||
emit('update:open', false);
|
||||
}
|
||||
|
||||
const fileList = ref<any[]>([]);
|
||||
@@ -87,7 +86,11 @@ const customRequest: UploadProps['httpRequest'] = async function (options) {
|
||||
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(`上传出错:${res.msg}`);
|
||||
onError?.(new Error(res.msg));
|
||||
const error = new Error(res.msg) as any;
|
||||
error.status = 200;
|
||||
error.method = 'POST';
|
||||
error.url = UPLOAD_URL;
|
||||
onError?.(error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,7 +99,7 @@ const customRequest: UploadProps['httpRequest'] = async function (options) {
|
||||
uploadData.title = '';
|
||||
uploadData.introduction = '';
|
||||
|
||||
emit('update:modelValue', false);
|
||||
emit('update:open', false);
|
||||
ElMessage.success('上传成功');
|
||||
onSuccess?.(res);
|
||||
emit('uploaded');
|
||||
@@ -109,7 +112,7 @@ const customRequest: UploadProps['httpRequest'] = async function (options) {
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="modelValue"
|
||||
:model-value="open"
|
||||
title="新建视频"
|
||||
width="600px"
|
||||
@close="handleCancel"
|
||||
|
||||
@@ -16,6 +16,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const columns = useImageGridColumns();
|
||||
@@ -37,6 +38,21 @@ const [Grid, gridApi] = useVbenVxeGrid<MpMaterialApi.Material>({
|
||||
refresh: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
// 数据由父组件管理,触发刷新事件后返回当前数据
|
||||
emit('refresh');
|
||||
// 返回当前数据,避免覆盖
|
||||
return {
|
||||
list: Array.isArray(props.list) ? props.list : [],
|
||||
total: props.list?.length || 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
autoLoad: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -55,7 +71,7 @@ watch(
|
||||
await nextTick();
|
||||
updateGridData(data);
|
||||
},
|
||||
{ flush: 'post' },
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
@@ -19,6 +19,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const columns = useVideoGridColumns();
|
||||
@@ -39,20 +40,40 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
// 数据由父组件管理,触发刷新事件后返回当前数据
|
||||
emit('refresh');
|
||||
// 返回当前数据,避免覆盖
|
||||
return {
|
||||
list: Array.isArray(props.list) ? props.list : [],
|
||||
total: props.list?.length || 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
autoLoad: false,
|
||||
},
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
function updateGridData(data: MpMaterialApi.Material[]) {
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(list: MpMaterialApi.Material[]) => {
|
||||
async (list: MpMaterialApi.Material[]) => {
|
||||
const data = Array.isArray(list) ? list : [];
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
await nextTick();
|
||||
updateGridData(data);
|
||||
},
|
||||
{ immediate: true },
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { openWindow } from '@vben/utils';
|
||||
@@ -19,6 +19,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const columns = useVoiceGridColumns();
|
||||
@@ -39,20 +40,40 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
// 数据由父组件管理,触发刷新事件后返回当前数据
|
||||
emit('refresh');
|
||||
// 返回当前数据,避免覆盖
|
||||
return {
|
||||
list: Array.isArray(props.list) ? props.list : [],
|
||||
total: props.list?.length || 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
autoLoad: false,
|
||||
},
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
function updateGridData(data: MpMaterialApi.Material[]) {
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(list: MpMaterialApi.Material[]) => {
|
||||
async (list: MpMaterialApi.Material[]) => {
|
||||
const data = Array.isArray(list) ? list : [];
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
await nextTick();
|
||||
updateGridData(data);
|
||||
},
|
||||
{ immediate: true },
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
|
||||
95
apps/web-ele/src/views/mp/message/data.ts
Normal file
95
apps/web-ele/src/views/mp/message/data.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMessageApi } from '#/api/mp/message';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'accountId',
|
||||
label: '公众号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '消息类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择消息类型',
|
||||
options: getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'openid',
|
||||
label: '用户标识',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户标识',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
type: 'datetimerange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<MpMessageApi.Message>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发送时间',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
slots: { default: 'createTime' },
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '消息类型',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'sendFrom',
|
||||
title: '发送方',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
slots: { default: 'sendFrom' },
|
||||
},
|
||||
{
|
||||
field: 'openid',
|
||||
title: '用户标识',
|
||||
width: 300,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
title: '内容',
|
||||
align: 'left',
|
||||
minWidth: 320,
|
||||
slots: { default: 'content' },
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,195 +1,229 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMessageApi } from '#/api/mp/message';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { DICT_TYPE, MpMsgType } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { MpMsgType as MsgType } from '@vben/constants';
|
||||
import { formatDate2 } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDatePicker,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
import { ElDialog, ElImage, ElTag } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getMessagePage } from '#/api/mp/message';
|
||||
import { WxAccountSelect, WxMsg } from '#/views/mp/components';
|
||||
import {
|
||||
WxAccountSelect,
|
||||
WxLocation,
|
||||
WxMsg,
|
||||
WxMusic,
|
||||
WxNews,
|
||||
WxVideoPlayer,
|
||||
WxVoicePlayer,
|
||||
} from '#/views/mp/components';
|
||||
|
||||
import MessageTable from './message-table.vue';
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'MpMessage' });
|
||||
|
||||
const loading = ref(false);
|
||||
const total = ref(0); // 数据的总页数
|
||||
const list = ref<any[]>([]); // 当前页的列表数据
|
||||
|
||||
const queryParams = reactive<{
|
||||
accountId: number;
|
||||
createTime: [Dayjs, Dayjs] | undefined;
|
||||
openid: string;
|
||||
pageNo: number;
|
||||
pageSize: number;
|
||||
type: string;
|
||||
}>({
|
||||
accountId: -1,
|
||||
createTime: undefined,
|
||||
openid: '',
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: MpMsgType.Text,
|
||||
}); // 搜索参数
|
||||
|
||||
const queryFormRef = ref(); // 搜索的表单
|
||||
|
||||
// 消息对话框
|
||||
const messageBoxVisible = ref(false);
|
||||
const messageBoxUserId = ref(0);
|
||||
|
||||
/** 侦听 accountId */
|
||||
function onAccountChanged(id: number) {
|
||||
queryParams.accountId = id;
|
||||
queryParams.pageNo = 1;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = await getMessagePage(queryParams);
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
async function resetQuery() {
|
||||
// 暂存 accountId,并在 reset 后恢复
|
||||
const accountId = queryParams.accountId;
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.accountId = accountId;
|
||||
handleQuery();
|
||||
/** 公众号变化时查询数据 */
|
||||
function handleAccountChange(accountId: number) {
|
||||
gridApi.formApi.setValues({ accountId });
|
||||
gridApi.formApi.submitForm();
|
||||
}
|
||||
|
||||
/** 打开消息发送窗口 */
|
||||
async function handleSend(userId: number) {
|
||||
function handleSend(userId: number) {
|
||||
messageBoxUserId.value = userId;
|
||||
messageBoxVisible.value = true;
|
||||
}
|
||||
|
||||
/** 分页改变事件 */
|
||||
function handlePageChange(page: number) {
|
||||
queryParams.pageNo = page;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 每页条数改变事件 */
|
||||
function handleSizeChange(pageSize: number) {
|
||||
queryParams.pageSize = pageSize;
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 显示总条数 */
|
||||
function showTotal(total: number) {
|
||||
return `共 ${total} 条`;
|
||||
}
|
||||
// TODO @dylan:是不是应该都用 Grid 哈?
|
||||
const [Grid, gridApi] = useVbenVxeGrid<MpMessageApi.Message>({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getMessagePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
autoLoad: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
} as VxeTableGridOptions<MpMessageApi.Message>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height class="flex flex-col">
|
||||
<!-- 搜索工作栏 -->
|
||||
<div class="mb-4 rounded-lg bg-background p-4">
|
||||
<ElForm
|
||||
ref="queryFormRef"
|
||||
:model="queryParams"
|
||||
:inline="true"
|
||||
class="search-form"
|
||||
>
|
||||
<ElFormItem label="公众号" prop="accountId">
|
||||
<WxAccountSelect @change="onAccountChanged" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="消息类型" prop="type">
|
||||
<ElSelect
|
||||
v-model="queryParams.type"
|
||||
placeholder="请选择消息类型"
|
||||
class="!w-[240px]"
|
||||
>
|
||||
<ElOption
|
||||
v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
:label="dict.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="用户标识" prop="openid">
|
||||
<ElInput
|
||||
v-model="queryParams.openid"
|
||||
placeholder="请输入用户标识"
|
||||
clearable
|
||||
class="!w-[240px]"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="创建时间" prop="createTime">
|
||||
<ElDatePicker
|
||||
v-model="queryParams.createTime"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
class="!w-[240px]"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:magnify" />
|
||||
</template>
|
||||
搜索
|
||||
</ElButton>
|
||||
<ElButton class="ml-2" @click="resetQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<template #form-accountId>
|
||||
<WxAccountSelect @change="handleAccountChange" />
|
||||
</template>
|
||||
<template #createTime="{ row }">
|
||||
{{ row.createTime ? formatDate2(row.createTime) : '' }}
|
||||
</template>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="flex-1 rounded-lg bg-background p-4">
|
||||
<MessageTable :list="list" :loading="loading" @send="handleSend" />
|
||||
<div v-show="total > 0" class="mt-4 flex justify-end">
|
||||
<ElPagination
|
||||
v-model:current-page="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:show-total="showTotal"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
<template #sendFrom="{ row }">
|
||||
<ElTag v-if="row.sendFrom === 1" type="success">粉丝</ElTag>
|
||||
<ElTag v-else>公众号</ElTag>
|
||||
</template>
|
||||
|
||||
<template #content="{ row }">
|
||||
<div
|
||||
v-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'subscribe'
|
||||
"
|
||||
>
|
||||
<ElTag type="success">关注</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'unsubscribe'
|
||||
"
|
||||
>
|
||||
<ElTag type="danger">取消关注</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'CLICK'
|
||||
"
|
||||
>
|
||||
<ElTag>点击菜单</ElTag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event && row.event === 'VIEW'">
|
||||
<ElTag>点击菜单链接</ElTag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'scancode_waitmsg'
|
||||
"
|
||||
>
|
||||
<ElTag>扫码结果</ElTag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'scancode_push'
|
||||
"
|
||||
>
|
||||
<ElTag>扫码结果</ElTag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Event && row.event === 'pic_sysphoto'"
|
||||
>
|
||||
<ElTag>系统拍照发图</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'pic_photo_or_album'
|
||||
"
|
||||
>
|
||||
<ElTag>拍照或者相册</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Event && row.event === 'pic_weixin'"
|
||||
>
|
||||
<ElTag>微信相册</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'location_select'
|
||||
"
|
||||
>
|
||||
<ElTag>选择地理位置</ElTag>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event">
|
||||
<ElTag type="danger">未知事件类型</ElTag>
|
||||
</div>
|
||||
|
||||
<div v-else-if="row.type === MsgType.Text">{{ row.content }}</div>
|
||||
<div v-else-if="row.type === MsgType.Voice">
|
||||
<WxVoicePlayer
|
||||
:url="row.mediaUrl || ''"
|
||||
:content="row.recognition || ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Image">
|
||||
<a :href="row.mediaUrl" target="_blank">
|
||||
<ElImage :src="row.mediaUrl" :width="100" :preview-src-list="[]" />
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Video || row.type === 'shortvideo'"
|
||||
>
|
||||
<WxVideoPlayer :url="row.mediaUrl || ''" class="mt-2" />
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Link">
|
||||
<ElTag>链接</ElTag>
|
||||
:
|
||||
<a :href="row.url" target="_blank">{{ row.title }}</a>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Location">
|
||||
<WxLocation
|
||||
:label="row.label || ''"
|
||||
:location-y="row.locationY || 0"
|
||||
:location-x="row.locationX || 0"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Music">
|
||||
<WxMusic
|
||||
:title="row.title"
|
||||
:description="row.description"
|
||||
:thumb-media-url="row.thumbMediaUrl || ''"
|
||||
:music-url="row.musicUrl"
|
||||
:hq-music-url="row.hqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.News">
|
||||
<WxNews :articles="row.articles" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<ElTag type="danger">未知消息类型</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '消息',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: () => handleSend(row.userId || 0),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 发送消息的弹窗 -->
|
||||
<ElDialog
|
||||
@@ -203,9 +237,3 @@ function showTotal(total: number) {
|
||||
</ElDialog>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMessageApi } from '#/api/mp/message';
|
||||
|
||||
import { onMounted, watch } from 'vue';
|
||||
|
||||
import { MpMsgType as MsgType } from '@vben/constants';
|
||||
import { formatDate2 } from '@vben/utils';
|
||||
|
||||
import { ElButton, ElImage, ElTag } from 'element-plus';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
WxLocation,
|
||||
WxMusic,
|
||||
WxNews,
|
||||
WxVideoPlayer,
|
||||
WxVoicePlayer,
|
||||
} from '#/views/mp/components';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list?: MpMessageApi.Message[];
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
list() {
|
||||
return [];
|
||||
},
|
||||
loading: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'send', userId: number): void;
|
||||
}>();
|
||||
|
||||
const columns: VxeTableGridOptions<MpMessageApi.Message>['columns'] = [
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发送时间',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
slots: { default: 'createTime' },
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '消息类型',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'sendFrom',
|
||||
title: '发送方',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
slots: { default: 'sendFrom' },
|
||||
},
|
||||
{
|
||||
field: 'openid',
|
||||
title: '用户标识',
|
||||
width: 300,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
title: '内容',
|
||||
align: 'left',
|
||||
minWidth: 320,
|
||||
slots: { default: 'content' },
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid<MpMessageApi.Message>({
|
||||
gridOptions: {
|
||||
border: true,
|
||||
columns,
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
});
|
||||
|
||||
function normalizeList(list?: MpMessageApi.Message[]) {
|
||||
return Array.isArray(list) ? list : [];
|
||||
}
|
||||
|
||||
function updateGridData(data: MpMessageApi.Message[]) {
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(list) => {
|
||||
updateGridData(normalizeList(list));
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(loading) => {
|
||||
gridApi.setLoading(loading);
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
updateGridData(normalizeList(props.list));
|
||||
gridApi.setLoading(props.loading);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #createTime="{ row }">
|
||||
{{ row.createTime ? formatDate2(row.createTime) : '' }}
|
||||
</template>
|
||||
|
||||
<template #sendFrom="{ row }">
|
||||
<ElTag v-if="row.sendFrom === 1" type="success">粉丝</ElTag>
|
||||
<ElTag v-else>公众号</ElTag>
|
||||
</template>
|
||||
|
||||
<template #content="{ row }">
|
||||
<div
|
||||
v-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'subscribe'
|
||||
"
|
||||
>
|
||||
<ElTag type="success">关注</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'unsubscribe'
|
||||
"
|
||||
>
|
||||
<ElTag type="danger">取消关注</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(row.type as string) === (MsgType.Event as string) &&
|
||||
(row.event as string) === 'CLICK'
|
||||
"
|
||||
>
|
||||
<ElTag>点击菜单</ElTag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event && row.event === 'VIEW'">
|
||||
<ElTag>点击菜单链接</ElTag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'scancode_waitmsg'
|
||||
"
|
||||
>
|
||||
<ElTag>扫码结果</ElTag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Event && row.event === 'scancode_push'"
|
||||
>
|
||||
<ElTag>扫码结果</ElTag>
|
||||
【{{ row.eventKey }}】
|
||||
</div>
|
||||
<div
|
||||
v-else-if="row.type === MsgType.Event && row.event === 'pic_sysphoto'"
|
||||
>
|
||||
<ElTag>系统拍照发图</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'pic_photo_or_album'
|
||||
"
|
||||
>
|
||||
<ElTag>拍照或者相册</ElTag>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event && row.event === 'pic_weixin'">
|
||||
<ElTag>微信相册</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
row.type === MsgType.Event && row.event === 'location_select'
|
||||
"
|
||||
>
|
||||
<ElTag>选择地理位置</ElTag>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Event">
|
||||
<ElTag type="danger">未知事件类型</ElTag>
|
||||
</div>
|
||||
|
||||
<div v-else-if="row.type === MsgType.Text">{{ row.content }}</div>
|
||||
<div v-else-if="row.type === MsgType.Voice">
|
||||
<WxVoicePlayer
|
||||
:url="row.mediaUrl || ''"
|
||||
:content="row.recognition || ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Image">
|
||||
<a :href="row.mediaUrl" target="_blank">
|
||||
<ElImage :src="row.mediaUrl" :width="100" :preview-src-list="[]" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Video || row.type === 'shortvideo'">
|
||||
<WxVideoPlayer :url="row.mediaUrl || ''" class="mt-2" />
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Link">
|
||||
<ElTag>链接</ElTag>
|
||||
:
|
||||
<a :href="row.url" target="_blank">{{ row.title }}</a>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Location">
|
||||
<WxLocation
|
||||
:label="row.label || ''"
|
||||
:location-y="row.locationY || 0"
|
||||
:location-x="row.locationX || 0"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.Music">
|
||||
<WxMusic
|
||||
:title="row.title"
|
||||
:description="row.description"
|
||||
:thumb-media-url="row.thumbMediaUrl || ''"
|
||||
:music-url="row.musicUrl"
|
||||
:hq-music-url="row.hqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="row.type === MsgType.News">
|
||||
<WxNews :articles="row.articles" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<ElTag type="danger">未知消息类型</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<ElButton type="primary" link @click="emit('send', row.userId || 0)">
|
||||
消息
|
||||
</ElButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
Reference in New Issue
Block a user