Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vben into cleaning
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { NewsType } from '@vben/constants';
|
||||
@@ -34,16 +34,6 @@ const emit = defineEmits<{
|
||||
(e: 'selectMaterial', item: any): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false); // 遮罩层
|
||||
const total = ref(0); // 总条数
|
||||
const list = ref<any[]>([]); // 数据列表
|
||||
const queryParams = reactive({
|
||||
accountId: props.accountId,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
}); // 查询参数
|
||||
|
||||
// TODO @dylan:可以把【点击上传】3 个 tab 的按钮,放到右侧的 toolbar 一起,和刷新按钮放在一行;
|
||||
const voiceGridColumns: VxeTableGridOptions<MpMaterialApi.Material>['columns'] =
|
||||
[
|
||||
{
|
||||
@@ -123,6 +113,99 @@ const videoGridColumns: VxeTableGridOptions<MpMaterialApi.Material>['columns'] =
|
||||
},
|
||||
];
|
||||
|
||||
// Image Grid
|
||||
const [ImageGrid, imageGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, { accountId }) => {
|
||||
const finalAccountId = accountId ?? props.accountId;
|
||||
if (!finalAccountId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getMaterialPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
accountId: finalAccountId,
|
||||
type: 'image',
|
||||
});
|
||||
},
|
||||
},
|
||||
autoLoad: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'mediaId',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
// News Grid
|
||||
const [NewsGrid, newsGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, { accountId }) => {
|
||||
const finalAccountId = accountId ?? props.accountId;
|
||||
if (!finalAccountId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
if (props.newsType === NewsType.Published) {
|
||||
const data = await getFreePublishPage({
|
||||
accountId: finalAccountId,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
data.list.forEach((item: any) => {
|
||||
const articles = item.content.newsItem;
|
||||
articles.forEach((article: any) => {
|
||||
article.picUrl = article.thumbUrl;
|
||||
});
|
||||
});
|
||||
return data;
|
||||
} else {
|
||||
const data = await getDraftPage({
|
||||
accountId: finalAccountId,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
data.list.forEach((draft: any) => {
|
||||
const articles = draft.content.newsItem;
|
||||
articles.forEach((article: any) => {
|
||||
article.picUrl = article.thumbUrl;
|
||||
});
|
||||
});
|
||||
return data;
|
||||
}
|
||||
},
|
||||
},
|
||||
autoLoad: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'mediaId',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<any>,
|
||||
});
|
||||
|
||||
const [VoiceGrid, voiceGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
border: true,
|
||||
@@ -136,7 +219,7 @@ const [VoiceGrid, voiceGridApi] = useVbenVxeGrid({
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, { accountId }) => {
|
||||
const finalAccountId = accountId ?? queryParams.accountId;
|
||||
const finalAccountId = accountId ?? props.accountId;
|
||||
if (!finalAccountId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
@@ -172,7 +255,7 @@ const [VideoGrid, videoGridApi] = useVbenVxeGrid({
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, { accountId }) => {
|
||||
const finalAccountId = accountId ?? queryParams.accountId;
|
||||
const finalAccountId = accountId ?? props.accountId;
|
||||
if (finalAccountId === undefined || finalAccountId === null) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
@@ -195,91 +278,164 @@ const [VideoGrid, videoGridApi] = useVbenVxeGrid({
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
// 从 Grid 获取数据
|
||||
const imageList = computed(() => {
|
||||
try {
|
||||
const tableData = imageGridApi.grid?.getTableData();
|
||||
return (tableData?.tableData as MpMaterialApi.Material[]) || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const imageLoading = computed(() => {
|
||||
return imageGridApi.grid?.loading || false;
|
||||
});
|
||||
|
||||
const imageTotal = computed(() => {
|
||||
try {
|
||||
const proxyInfo = imageGridApi.grid?.getProxyInfo();
|
||||
return proxyInfo?.pager?.total || 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const imageCurrentPage = computed({
|
||||
get: () => {
|
||||
try {
|
||||
return imageGridApi.grid?.pagerConfig?.currentPage || 1;
|
||||
} catch {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
set: (value: number) => {
|
||||
imageGridApi.grid?.commitProxy('page', { currentPage: value });
|
||||
},
|
||||
});
|
||||
|
||||
const imagePageSize = computed({
|
||||
get: () => {
|
||||
try {
|
||||
return imageGridApi.grid?.pagerConfig?.pageSize || 10;
|
||||
} catch {
|
||||
return 10;
|
||||
}
|
||||
},
|
||||
set: (value: number) => {
|
||||
imageGridApi.grid?.commitProxy('page', { pageSize: value, currentPage: 1 });
|
||||
},
|
||||
});
|
||||
|
||||
const newsList = computed(() => {
|
||||
try {
|
||||
const tableData = newsGridApi.grid?.getTableData();
|
||||
return (tableData?.tableData as any[]) || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const newsLoading = computed(() => {
|
||||
return newsGridApi.grid?.loading || false;
|
||||
});
|
||||
|
||||
const newsTotal = computed(() => {
|
||||
try {
|
||||
const proxyInfo = newsGridApi.grid?.getProxyInfo();
|
||||
return proxyInfo?.pager?.total || 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const newsCurrentPage = computed({
|
||||
get: () => {
|
||||
try {
|
||||
return newsGridApi.grid?.pagerConfig?.currentPage || 1;
|
||||
} catch {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
set: (value: number) => {
|
||||
newsGridApi.grid?.commitProxy('page', { currentPage: value });
|
||||
},
|
||||
});
|
||||
|
||||
const newsPageSize = computed({
|
||||
get: () => {
|
||||
try {
|
||||
return newsGridApi.grid?.pagerConfig?.pageSize || 10;
|
||||
} catch {
|
||||
return 10;
|
||||
}
|
||||
},
|
||||
set: (value: number) => {
|
||||
newsGridApi.grid?.commitProxy('page', { pageSize: value, currentPage: 1 });
|
||||
},
|
||||
});
|
||||
|
||||
function selectMaterialFun(item: any) {
|
||||
emit('selectMaterial', item);
|
||||
}
|
||||
|
||||
async function getMaterialPageFun() {
|
||||
const data = await getMaterialPage({
|
||||
...queryParams,
|
||||
type: props.type,
|
||||
});
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
}
|
||||
|
||||
async function getFreePublishPageFun() {
|
||||
const data = await getFreePublishPage(queryParams);
|
||||
data.list.forEach((item: any) => {
|
||||
const articles = item.content.newsItem;
|
||||
articles.forEach((article: any) => {
|
||||
article.picUrl = article.thumbUrl;
|
||||
});
|
||||
});
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
}
|
||||
|
||||
async function getDraftPageFun() {
|
||||
const data = await getDraftPage(queryParams);
|
||||
data.list.forEach((draft: any) => {
|
||||
const articles = draft.content.newsItem;
|
||||
articles.forEach((article: any) => {
|
||||
article.picUrl = article.thumbUrl;
|
||||
});
|
||||
});
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
}
|
||||
|
||||
async function getPage() {
|
||||
if (props.type === 'voice') {
|
||||
await voiceGridApi.reload({ accountId: queryParams.accountId });
|
||||
return;
|
||||
}
|
||||
if (props.type === 'video') {
|
||||
await videoGridApi.reload({ accountId: queryParams.accountId });
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
if (props.type === 'news' && props.newsType === NewsType.Published) {
|
||||
await getFreePublishPageFun();
|
||||
} else if (props.type === 'news' && props.newsType === NewsType.Draft) {
|
||||
await getDraftPageFun();
|
||||
} else {
|
||||
await getMaterialPageFun();
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 accountId 变化
|
||||
watch(
|
||||
() => props.accountId,
|
||||
(accountId) => {
|
||||
queryParams.accountId = accountId;
|
||||
queryParams.pageNo = 1;
|
||||
getPage();
|
||||
switch (props.type) {
|
||||
case 'image': {
|
||||
imageGridApi.reload({ accountId });
|
||||
break;
|
||||
}
|
||||
case 'news': {
|
||||
newsGridApi.reload({ accountId });
|
||||
break;
|
||||
}
|
||||
case 'video': {
|
||||
videoGridApi.reload({ accountId });
|
||||
break;
|
||||
}
|
||||
case 'voice': {
|
||||
voiceGridApi.reload({ accountId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 监听 type 变化
|
||||
watch(
|
||||
() => props.type,
|
||||
() => {
|
||||
queryParams.pageNo = 1;
|
||||
getPage();
|
||||
switch (props.type) {
|
||||
case 'image': {
|
||||
imageGridApi.reload({ accountId: props.accountId });
|
||||
break;
|
||||
}
|
||||
case 'news': {
|
||||
newsGridApi.reload({ accountId: props.accountId });
|
||||
break;
|
||||
}
|
||||
case 'video': {
|
||||
videoGridApi.reload({ accountId: props.accountId });
|
||||
break;
|
||||
}
|
||||
case 'voice': {
|
||||
voiceGridApi.reload({ accountId: props.accountId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 newsType 变化
|
||||
watch(
|
||||
() => props.newsType,
|
||||
() => {
|
||||
if (props.type === 'news') {
|
||||
queryParams.pageNo = 1;
|
||||
getPage();
|
||||
newsGridApi.reload({ accountId: props.accountId });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -289,42 +445,47 @@ watch(
|
||||
<Page :bordered="false" class="pb-8">
|
||||
<!-- 类型:image -->
|
||||
<template v-if="props.type === 'image'">
|
||||
<Spin :spinning="loading">
|
||||
<div
|
||||
class="mx-auto w-full columns-1 [column-gap:10px] md:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5"
|
||||
>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.mediaId"
|
||||
class="mb-2.5 h-72 break-inside-avoid border border-[#eaeaea] p-2.5"
|
||||
>
|
||||
<img
|
||||
class="h-48 w-full object-contain"
|
||||
:src="item.url"
|
||||
alt="素材图片"
|
||||
<div class="image-grid-wrapper">
|
||||
<ImageGrid>
|
||||
<template #default>
|
||||
<Spin :spinning="imageLoading">
|
||||
<div
|
||||
class="mx-auto w-full columns-1 [column-gap:10px] md:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5"
|
||||
>
|
||||
<div
|
||||
v-for="item in imageList"
|
||||
:key="item.mediaId"
|
||||
class="mb-2.5 h-72 break-inside-avoid border border-[#eaeaea] p-2.5"
|
||||
>
|
||||
<img
|
||||
class="h-48 w-full object-contain"
|
||||
:src="item.url"
|
||||
alt="素材图片"
|
||||
/>
|
||||
<p class="truncate text-center text-xs leading-[30px]">
|
||||
{{ item.name }}
|
||||
</p>
|
||||
<Row class="flex justify-center pt-2.5">
|
||||
<Button type="primary" @click="selectMaterialFun(item)">
|
||||
选择
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:circle-check" />
|
||||
</template>
|
||||
</Button>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
<Pagination
|
||||
v-model:current="imageCurrentPage"
|
||||
v-model:page-size="imagePageSize"
|
||||
:total="imageTotal"
|
||||
class="mt-4"
|
||||
show-size-changer
|
||||
/>
|
||||
<p class="truncate text-center text-xs leading-[30px]">
|
||||
{{ item.name }}
|
||||
</p>
|
||||
<Row class="flex justify-center pt-2.5">
|
||||
<Button type="primary" @click="selectMaterialFun(item)">
|
||||
选择
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:circle-check" />
|
||||
</template>
|
||||
</Button>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
<Pagination
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
class="mt-4"
|
||||
@change="getPage"
|
||||
@show-size-change="getPage"
|
||||
/>
|
||||
</template>
|
||||
</ImageGrid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 类型:voice -->
|
||||
@@ -363,37 +524,52 @@ watch(
|
||||
|
||||
<!-- 类型:news -->
|
||||
<template v-else-if="props.type === 'news'">
|
||||
<Spin :spinning="loading">
|
||||
<div
|
||||
class="mx-auto w-full columns-1 [column-gap:10px] md:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5"
|
||||
>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.mediaId"
|
||||
class="mb-2.5 break-inside-avoid border border-[#eaeaea] p-2.5"
|
||||
>
|
||||
<div v-if="item.content && item.content.newsItem">
|
||||
<WxNews :articles="item.content.newsItem" />
|
||||
<Row class="flex justify-center pt-2.5">
|
||||
<Button type="primary" @click="selectMaterialFun(item)">
|
||||
选择
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:circle-check" />
|
||||
</template>
|
||||
</Button>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
<Pagination
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
class="mt-4"
|
||||
@change="getPage"
|
||||
@show-size-change="getPage"
|
||||
/>
|
||||
<div class="news-grid-wrapper">
|
||||
<NewsGrid>
|
||||
<template #default>
|
||||
<Spin :spinning="newsLoading">
|
||||
<div
|
||||
class="mx-auto w-full columns-1 [column-gap:10px] md:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5"
|
||||
>
|
||||
<div
|
||||
v-for="item in newsList"
|
||||
:key="item.mediaId"
|
||||
class="mb-2.5 break-inside-avoid border border-[#eaeaea] p-2.5"
|
||||
>
|
||||
<div v-if="item.content && item.content.newsItem">
|
||||
<WxNews :articles="item.content.newsItem" />
|
||||
<Row class="flex justify-center pt-2.5">
|
||||
<Button type="primary" @click="selectMaterialFun(item)">
|
||||
选择
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:circle-check" />
|
||||
</template>
|
||||
</Button>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
<Pagination
|
||||
v-model:current="newsCurrentPage"
|
||||
v-model:page-size="newsPageSize"
|
||||
:total="newsTotal"
|
||||
class="mt-4"
|
||||
show-size-changer
|
||||
/>
|
||||
</template>
|
||||
</NewsGrid>
|
||||
</div>
|
||||
</template>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.image-grid-wrapper :deep(.vxe-grid--body-wrapper) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.news-grid-wrapper :deep(.vxe-grid--body-wrapper) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
.mp-card {
|
||||
&__item {
|
||||
box-sizing: border-box;
|
||||
height: 200px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
font-feature-settings: 'tnum';
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
color: rgb(0 0 0 / 65%);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
||||
&:hover {
|
||||
border-color: rgb(0 0 0 / 9%);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 9%);
|
||||
}
|
||||
|
||||
&--add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
background-color: #fff;
|
||||
border: 1px dashed #000;
|
||||
border-color: #d9d9d9;
|
||||
border-radius: 2px;
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
background-color: #fff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
&__detail {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-right: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 48px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: -webkit-box;
|
||||
height: 64px;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 3;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&__menu {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
text-align: center;
|
||||
background: #f7f9fa;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** joolun 额外加的 */
|
||||
.mp-comment__main {
|
||||
flex: unset !important;
|
||||
margin: 0 8px !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
.mp-comment__header {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.mp-comment__body {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */
|
||||
.mp-comment {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&--reverse {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.mp-comment__main {
|
||||
&::before,
|
||||
&::after {
|
||||
right: -8px;
|
||||
left: auto;
|
||||
border-width: 8px 0 8px 8px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-left-color: #dedede;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-right: 1px;
|
||||
margin-left: auto;
|
||||
border-left-color: #f8f8f8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
box-sizing: border-box;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
vertical-align: middle;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 15px;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
&__author {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&__main {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
margin: 0 20px;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 2px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 100%;
|
||||
left: -8px;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
content: ' ';
|
||||
border-color: transparent;
|
||||
border-style: solid solid outset;
|
||||
border-width: 8px 8px 8px 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
z-index: 1;
|
||||
border-right-color: #dedede;
|
||||
}
|
||||
|
||||
&::after {
|
||||
z-index: 2;
|
||||
margin-left: 1px;
|
||||
border-right-color: #f8f8f8;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
font-family:
|
||||
'Segoe UI', 'Lucida Grande', Helvetica, Arial, 'Microsoft YaHei',
|
||||
FreeSans, Arimo, 'Droid Sans', 'wenquanyi micro hei', 'Hiragino Sans GB',
|
||||
'Hiragino Sans GB W3', FontAwesome, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 1px 0 1px 15px;
|
||||
margin: 0;
|
||||
font-family:
|
||||
Georgia, 'Times New Roman', Times, Kai, 'Kaiti SC', KaiTi, BiauKai,
|
||||
FontAwesome, serif;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply;
|
||||
newsType: NewsType;
|
||||
newsType?: NewsType;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { nextTick, onMounted, watch } from 'vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { useImageGridColumns } from './data';
|
||||
|
||||
const props = defineProps<{
|
||||
list: MpMaterialApi.Material[];
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
}>();
|
||||
|
||||
const columns = useImageGridColumns();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid<MpMaterialApi.Material>({
|
||||
gridOptions: {
|
||||
border: true,
|
||||
columns,
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
height: 220,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
});
|
||||
|
||||
function updateGridData(data: MpMaterialApi.Material[]) {
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
async (list: MpMaterialApi.Material[]) => {
|
||||
const data = Array.isArray(list) ? list : [];
|
||||
await nextTick();
|
||||
updateGridData(data);
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(loading: boolean) => {
|
||||
gridApi.setLoading(loading);
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
updateGridData(Array.isArray(props.list) ? props.list : []);
|
||||
gridApi.setLoading(props.loading);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid class="image-table-grid mt-4 pb-0">
|
||||
<template #image="{ row }">
|
||||
<div class="flex items-center justify-center" style="height: 192px">
|
||||
<img
|
||||
:src="row.url"
|
||||
class="object-contain"
|
||||
style="display: block; max-width: 100%; max-height: 192px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mp:material:delete'],
|
||||
popConfirm: {
|
||||
title: '确定要删除该图片吗?',
|
||||
confirm: () => emit('delete', row.id!),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -1,92 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { WxVideoPlayer } from '#/views/mp/components';
|
||||
|
||||
import { useVideoGridColumns } from './data';
|
||||
|
||||
const props = defineProps<{
|
||||
list: MpMaterialApi.Material[];
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
}>();
|
||||
|
||||
const columns = useVideoGridColumns();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
border: true,
|
||||
columns,
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(list: MpMaterialApi.Material[]) => {
|
||||
const data = Array.isArray(list) ? list : [];
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(loading: boolean) => {
|
||||
gridApi.setLoading(loading);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid class="mt-4">
|
||||
<template #video="{ row }">
|
||||
<WxVideoPlayer v-if="row.url" :url="row.url" />
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '下载',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
onClick: () => openWindow(row.url),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mp:material:delete'],
|
||||
popConfirm: {
|
||||
title: '确定要删除该视频吗?',
|
||||
confirm: () => emit('delete', row.id!),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -1,93 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { WxVoicePlayer } from '#/views/mp/components';
|
||||
|
||||
import { useVoiceGridColumns } from './data';
|
||||
// TODO @dylan:组件内,尽量用 modules 哈。只有对外共享,才用 components
|
||||
|
||||
const props = defineProps<{
|
||||
list: MpMaterialApi.Material[];
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [v: number];
|
||||
}>();
|
||||
|
||||
const columns = useVoiceGridColumns();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
border: true,
|
||||
columns,
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
showOverflow: 'tooltip',
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(list: MpMaterialApi.Material[]) => {
|
||||
const data = Array.isArray(list) ? list : [];
|
||||
if (gridApi.grid?.loadData) {
|
||||
gridApi.grid.loadData(data);
|
||||
} else {
|
||||
gridApi.setGridOptions({ data });
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(loading: boolean) => {
|
||||
gridApi.setLoading(loading);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid class="mt-4">
|
||||
<template #voice="{ row }">
|
||||
<WxVoicePlayer v-if="row.url" :url="row.url" />
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '下载',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
onClick: () => openWindow(row.url),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mp:material:delete'],
|
||||
popConfirm: {
|
||||
title: '确定要删除该语音吗?',
|
||||
confirm: () => emit('delete', row.id!),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -1,78 +1,125 @@
|
||||
<script lang="ts" setup>
|
||||
import { provide, reactive, ref } from 'vue';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
import { provide, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { confirm, DocAlert, Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Button, Card, Form, message, Pagination, Tabs } from 'ant-design-vue';
|
||||
import { Button, message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deletePermanentMaterial, getMaterialPage } from '#/api/mp/material';
|
||||
import { WxAccountSelect } from '#/views/mp/components';
|
||||
|
||||
import ImageTable from './components/image-table.vue';
|
||||
import { UploadType } from './components/upload';
|
||||
import UploadFile from './components/UploadFile.vue';
|
||||
import UploadVideo from './components/UploadVideo.vue';
|
||||
import VideoTable from './components/video-table.vue';
|
||||
import VoiceTable from './components/voice-table.vue';
|
||||
import {
|
||||
useGridFormSchema,
|
||||
useImageGridColumns,
|
||||
useVideoGridColumns,
|
||||
useVoiceGridColumns,
|
||||
} from './modules/data';
|
||||
import { UploadType } from './modules/upload';
|
||||
import UploadFile from './modules/UploadFile.vue';
|
||||
import UploadVideo from './modules/UploadVideo.vue';
|
||||
|
||||
defineOptions({ name: 'MpMaterial' });
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const type = ref<UploadType>(UploadType.Image); // 素材类型
|
||||
const loading = ref(false); // 遮罩层
|
||||
const list = ref<any[]>([]); // 数据列表
|
||||
const total = ref(0); // 总条数
|
||||
const showCreateVideo = ref(false); // 是否新建视频的弹窗
|
||||
|
||||
const accountId = ref(-1);
|
||||
provide('accountId', accountId);
|
||||
|
||||
const queryParams = reactive({
|
||||
accountId,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
permanent: true,
|
||||
}); // 查询参数
|
||||
const showCreateVideo = ref(false); // 是否新建视频的弹窗
|
||||
|
||||
/** 侦听公众号变化 */
|
||||
function onAccountChanged(id: number) {
|
||||
accountId.value = id;
|
||||
queryParams.accountId = id;
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getMaterialPage({
|
||||
...queryParams,
|
||||
type: type.value,
|
||||
});
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
// 根据类型获取对应的列配置
|
||||
const getColumnsByType = () => {
|
||||
switch (type.value) {
|
||||
case UploadType.Image: {
|
||||
return useImageGridColumns();
|
||||
}
|
||||
case UploadType.Video: {
|
||||
return useVideoGridColumns();
|
||||
}
|
||||
case UploadType.Voice: {
|
||||
return useVoiceGridColumns();
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: getColumnsByType(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const finalAccountId = formValues?.accountId ?? accountId.value;
|
||||
if (!finalAccountId || finalAccountId === -1) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getMaterialPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
type: type.value,
|
||||
permanent: true,
|
||||
accountId: finalAccountId,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
autoLoad: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
cellConfig: {
|
||||
height: type.value === UploadType.Image ? 220 : undefined,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MpMaterialApi.Material>,
|
||||
});
|
||||
|
||||
// 当 tab 切换时,更新 Grid 的 columns 和 rowConfig
|
||||
async function onTabChange() {
|
||||
const columns = getColumnsByType();
|
||||
gridApi.setGridOptions({
|
||||
columns,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
cellConfig: {
|
||||
height: type.value === UploadType.Image ? 220 : undefined,
|
||||
},
|
||||
});
|
||||
await gridApi.reload();
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
async function handleAccountChange(id: number) {
|
||||
accountId.value = id;
|
||||
// 同步设置表单值
|
||||
await gridApi.formApi.setValues({ accountId: id });
|
||||
await gridApi.formApi.submitForm();
|
||||
}
|
||||
|
||||
/** 处理 tab 切换 */
|
||||
function onTabChange() {
|
||||
// 提前清空数据,避免 tab 切换后显示垃圾数据
|
||||
list.value = [];
|
||||
total.value = 0;
|
||||
// 从第一页开始查询
|
||||
handleQuery();
|
||||
async function handleRefresh() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
/** 处理删除操作 */
|
||||
@@ -85,7 +132,7 @@ async function handleDelete(id: number) {
|
||||
try {
|
||||
await deletePermanentMaterial(id);
|
||||
message.success('删除成功');
|
||||
await getList();
|
||||
await handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
@@ -97,18 +144,12 @@ async function handleDelete(id: number) {
|
||||
<template #doc>
|
||||
<DocAlert title="公众号素材" url="https://doc.iocoder.cn/mp/material/" />
|
||||
</template>
|
||||
<div class="h-full">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Card class="h-[10%]" :bordered="false">
|
||||
<Form :model="queryParams" layout="inline">
|
||||
<Form.Item label="公众号">
|
||||
<WxAccountSelect @change="onAccountChanged" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card :bordered="false" class="mt-4 h-auto">
|
||||
<Tabs v-model:active-key="type" @change="onTabChange">
|
||||
<Grid class="material-grid">
|
||||
<template #form-accountId>
|
||||
<WxAccountSelect @change="handleAccountChange" />
|
||||
</template>
|
||||
<template #toolbar-actions>
|
||||
<Tabs v-model:active-key="type" class="w-full" @change="onTabChange">
|
||||
<!-- tab 1:图片 -->
|
||||
<Tabs.TabPane :key="UploadType.Image">
|
||||
<template #tab>
|
||||
@@ -117,30 +158,6 @@ async function handleDelete(id: number) {
|
||||
图片
|
||||
</span>
|
||||
</template>
|
||||
<UploadFile
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
:type="UploadType.Image"
|
||||
@uploaded="getList"
|
||||
>
|
||||
支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M
|
||||
</UploadFile>
|
||||
<!-- 列表 -->
|
||||
<ImageTable
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<!-- 分页组件 -->
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
@show-size-change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- tab 2:语音 -->
|
||||
@@ -151,30 +168,6 @@ async function handleDelete(id: number) {
|
||||
语音
|
||||
</span>
|
||||
</template>
|
||||
<UploadFile
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
:type="UploadType.Voice"
|
||||
@uploaded="getList"
|
||||
>
|
||||
格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s
|
||||
</UploadFile>
|
||||
<!-- 列表 -->
|
||||
<VoiceTable
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<!-- 分页组件 -->
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
@show-size-change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- tab 3:视频 -->
|
||||
@@ -185,35 +178,81 @@ async function handleDelete(id: number) {
|
||||
视频
|
||||
</span>
|
||||
</template>
|
||||
<Button
|
||||
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
|
||||
type="primary"
|
||||
@click="showCreateVideo = true"
|
||||
>
|
||||
新建视频
|
||||
</Button>
|
||||
<!-- 新建视频的弹窗 -->
|
||||
<UploadVideo v-model:open="showCreateVideo" @uploaded="getList" />
|
||||
<!-- 列表 -->
|
||||
<VideoTable
|
||||
:list="list"
|
||||
:loading="loading"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<!-- 分页组件 -->
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
@show-size-change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<UploadFile
|
||||
v-if="
|
||||
hasAccessByCodes(['mp:material:upload-permanent']) &&
|
||||
type === UploadType.Image
|
||||
"
|
||||
:type="UploadType.Image"
|
||||
@uploaded="handleRefresh"
|
||||
/>
|
||||
<UploadFile
|
||||
v-if="
|
||||
hasAccessByCodes(['mp:material:upload-permanent']) &&
|
||||
type === UploadType.Voice
|
||||
"
|
||||
:type="UploadType.Voice"
|
||||
@uploaded="handleRefresh"
|
||||
/>
|
||||
<Button
|
||||
v-if="
|
||||
hasAccessByCodes(['mp:material:upload-permanent']) &&
|
||||
type === UploadType.Video
|
||||
"
|
||||
type="primary"
|
||||
@click="showCreateVideo = true"
|
||||
>
|
||||
新建视频
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<!-- 图片列的 slot -->
|
||||
<template #image="{ row }">
|
||||
<div class="flex items-center justify-center" style="height: 192px">
|
||||
<img
|
||||
:src="row.url"
|
||||
class="object-contain"
|
||||
style="display: block; max-width: 100%; max-height: 192px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 语音列的 slot -->
|
||||
<template #voice="{ row }">
|
||||
<audio :src="row.url" controls style="width: 160px"></audio>
|
||||
</template>
|
||||
|
||||
<!-- 视频列的 slot -->
|
||||
<template #video="{ row }">
|
||||
<video
|
||||
:src="row.url"
|
||||
controls
|
||||
style="width: 200px; height: 150px"
|
||||
></video>
|
||||
</template>
|
||||
|
||||
<!-- 操作列的 slot -->
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['mp:material:delete'],
|
||||
onClick: () => handleDelete(row.id!),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 新建视频的弹窗 -->
|
||||
<UploadVideo v-model:open="showCreateVideo" @uploaded="handleRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -86,7 +86,6 @@ const customRequest: UploadProps['customRequest'] = async function (options) {
|
||||
:file-list="fileList"
|
||||
:headers="HEADERS"
|
||||
:multiple="true"
|
||||
class="mb-4"
|
||||
>
|
||||
<Button type="primary">
|
||||
<IconifyIcon icon="lucide:upload" class="mr-1" />
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MpMaterialApi } from '#/api/mp/material';
|
||||
|
||||
// TODO @dylan:看看 ele 要迁移一个么?
|
||||
/** 视频表格列配置 */
|
||||
export function useVideoGridColumns(): VxeTableGridOptions<MpMaterialApi.Material>['columns'] {
|
||||
return [
|
||||
@@ -132,3 +132,14 @@ export function useImageGridColumns(): VxeTableGridOptions<MpMaterialApi.Materia
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'accountId',
|
||||
label: '公众号',
|
||||
component: 'Input',
|
||||
},
|
||||
];
|
||||
}
|
||||
94
apps/web-antd/src/views/mp/message/data.ts
Normal file
94
apps/web-antd/src/views/mp/message/data.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
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';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
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: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: 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,184 +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} 条`;
|
||||
}
|
||||
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
|
||||
@@ -192,9 +236,3 @@ function showTotal(total: number) {
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-form :deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,262 +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';
|
||||
|
||||
// TODO @dylan:看看 ele 要迁移一个么?
|
||||
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>
|
||||
Reference in New Issue
Block a user