!292 fix: todo 处理

Merge pull request !292 from dylanmay/dev
This commit is contained in:
芋道源码
2025-12-07 04:36:19 +00:00
committed by Gitee
16 changed files with 792 additions and 908 deletions

View File

@@ -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'])"

View File

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

View File

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

View File

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

View 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' },
},
];
}

View File

@@ -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 哈1message-table 大部分合并到 index.vue2message-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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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' },
},
];
}

View File

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

View File

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