Merge pull request !299 from dylanmay/dev
This commit is contained in:
芋道源码
2025-12-15 06:17:56 +00:00
committed by Gitee
12 changed files with 991 additions and 1326 deletions

View File

@@ -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;
}>();
// TODO @dlyan @AI这里是不是应该都用 grid
const loading = ref(false); // 遮罩层
const total = ref(0); // 总条数
const list = ref<any[]>([]); // 数据列表
const queryParams = reactive({
accountId: props.accountId,
pageNo: 1,
pageSize: 10,
}); // 查询参数
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>

View File

@@ -1,79 +1,120 @@
<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 { computed, provide, ref } from 'vue';
import { useAccess } from '@vben/access';
import { confirm, DocAlert, Page } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
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 './modules/image-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';
import VideoTable from './modules/video-table.vue';
import VoiceTable from './modules/voice-table.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);
// TODO @dlyan @AI这里是不是应该都用 grid类似 yudao-ui-admin-vben-v5/apps/web-ele/src/views/mp/autoReply/index.vue
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,
height: type.value === UploadType.Image ? 220 : 'auto',
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MpMaterialApi.Material>,
});
// 当 tab 切换时,更新 Grid 的 columns 和 rowConfig
async function onTabChange() {
const columns = getColumnsByType();
await gridApi.setGridOptions({
columns,
rowConfig: {
keyField: 'id',
isHover: true,
height: type.value === UploadType.Image ? 220 : 'auto',
},
});
await gridApi.reload();
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.pageNo = 1;
getList();
async function handleAccountChange(id: number) {
accountId.value = id;
// 同步设置表单值
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();
}
/** 处理删除操作 */
@@ -86,7 +127,7 @@ async function handleDelete(id: number) {
try {
await deletePermanentMaterial(id);
message.success('删除成功');
await getList();
handleRefresh();
} finally {
hideLoading();
}
@@ -98,18 +139,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>
@@ -118,33 +153,6 @@ async function handleDelete(id: number) {
图片
</span>
</template>
<!-- 列表 -->
<ImageTable
:key="`image-${type}`"
:list="list"
:loading="loading"
@delete="handleDelete"
@refresh="getList"
>
<template #toolbar-tools>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Image"
@uploaded="getList"
/>
</template>
</ImageTable>
<!-- 分页组件 -->
<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语音 -->
@@ -155,33 +163,6 @@ async function handleDelete(id: number) {
语音
</span>
</template>
<!-- 列表 -->
<VoiceTable
:key="`voice-${type}`"
:list="list"
:loading="loading"
@delete="handleDelete"
@refresh="getList"
>
<template #toolbar-tools>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Voice"
@uploaded="getList"
/>
</template>
</VoiceTable>
<!-- 分页组件 -->
<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视频 -->
@@ -192,40 +173,80 @@ async function handleDelete(id: number) {
视频
</span>
</template>
<!-- 列表 -->
<VideoTable
:key="`video-${type}`"
:list="list"
:loading="loading"
@delete="handleDelete"
@refresh="getList"
>
<template #toolbar-tools>
<Button
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
type="primary"
@click="showCreateVideo = true"
>
新建视频
</Button>
</template>
</VideoTable>
<!-- 新建视频的弹窗 -->
<UploadVideo v-model:open="showCreateVideo" @uploaded="getList" />
<!-- 分页组件 -->
<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: '删除',
icon: ACTION_ICON.delete,
color: 'danger',
auth: ['mp:material:delete'],
onClick: () => handleDelete(row.id),
},
]"
/>
</template>
</Grid>
<!-- 新建视频的弹窗 -->
<UploadVideo v-model:open="showCreateVideo" @uploaded="handleRefresh" />
</Page>
</template>

View File

@@ -1,3 +1,4 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
@@ -131,3 +132,14 @@ export function useImageGridColumns(): VxeTableGridOptions<MpMaterialApi.Materia
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'accountId',
label: '公众号',
component: 'Input',
},
];
}

View File

@@ -1,123 +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';
import { $t } from '@vben/locales';
const props = defineProps<{
list: MpMaterialApi.Material[];
loading: boolean;
}>();
const emit = defineEmits<{
delete: [v: number];
refresh: [];
}>();
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,
},
toolbarConfig: {
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,
},
},
});
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);
},
{ immediate: true, 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 #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<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: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该图片吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -1,120 +0,0 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
import { nextTick, watch } from 'vue';
import { $t } from '@vben/locales';
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];
refresh: [];
}>();
const columns = useVideoGridColumns();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns,
keepSource: true,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
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,
async (list: MpMaterialApi.Material[]) => {
const data = Array.isArray(list) ? list : [];
await nextTick();
updateGridData(data);
},
{ immediate: true, flush: 'post' },
);
watch(
() => props.loading,
(loading: boolean) => {
gridApi.setLoading(loading);
},
{ immediate: true },
);
</script>
<template>
<Grid class="mt-4">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<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: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该视频吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -1,120 +0,0 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
import { nextTick, watch } from 'vue';
import { $t } from '@vben/locales';
import { openWindow } from '@vben/utils';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { WxVoicePlayer } from '#/views/mp/components';
import { useVoiceGridColumns } from './data';
const props = defineProps<{
list: MpMaterialApi.Material[];
loading: boolean;
}>();
const emit = defineEmits<{
delete: [v: number];
refresh: [];
}>();
const columns = useVoiceGridColumns();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns,
keepSource: true,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
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,
async (list: MpMaterialApi.Material[]) => {
const data = Array.isArray(list) ? list : [];
await nextTick();
updateGridData(data);
},
{ immediate: true, flush: 'post' },
);
watch(
() => props.loading,
(loading: boolean) => {
gridApi.setLoading(loading);
},
{ immediate: true },
);
</script>
<template>
<Grid class="mt-4">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<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: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该语音吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -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 { NewsType } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
@@ -33,16 +33,6 @@ const emit = defineEmits<{
(e: 'selectMaterial', item: any): void;
}>();
const loading = ref(false); // 遮罩层
const total = ref(0); // 总条数
const list = ref<any[]>([]); // 数据列表
// TODO @dlyan @AI这里是不是应该都用 grid
const queryParams = reactive({
accountId: props.accountId,
pageNo: 1,
pageSize: 10,
}); // 查询参数
const voiceGridColumns: VxeTableGridOptions<MpMaterialApi.Material>['columns'] =
[
{
@@ -122,6 +112,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,
@@ -135,7 +218,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 };
}
@@ -171,7 +254,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 };
}
@@ -194,91 +277,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 });
}
},
);
@@ -287,38 +443,41 @@ watch(
<template>
<div class="pb-30px">
<!-- 类型image -->
<div v-if="props.type === 'image'">
<div
class="mx-auto w-full columns-1 [column-gap:10px] md:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5"
v-loading="loading"
>
<div
class="mb-2.5 break-inside-avoid border border-[#eaeaea] p-2.5"
v-for="item in list"
:key="item.mediaId"
>
<img class="w-full" :src="item.url" />
<p class="truncate text-center text-xs leading-[30px]">
{{ item.name }}
</p>
<ElRow class="flex justify-center pt-2.5">
<ElButton type="success" @click="selectMaterialFun(item)">
选择
<IconifyIcon icon="lucide:circle-check" />
</ElButton>
</ElRow>
</div>
</div>
<!-- 分页组件 -->
<ElPagination
background
layout="prev, pager, next, sizes, total"
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@current-change="getMaterialPageFun"
@size-change="getMaterialPageFun"
/>
<div v-if="props.type === 'image'" class="image-grid-wrapper">
<ImageGrid>
<template #default>
<div
class="mx-auto w-full columns-1 [column-gap:10px] md:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5"
v-loading="imageLoading"
>
<div
class="mb-2.5 break-inside-avoid border border-[#eaeaea] p-2.5"
v-for="item in imageList"
:key="item.mediaId"
>
<img class="w-full" :src="item.url" />
<p class="truncate text-center text-xs leading-[30px]">
{{ item.name }}
</p>
<ElRow class="flex justify-center pt-2.5">
<ElButton type="success" @click="selectMaterialFun(item)">
选择
<IconifyIcon icon="lucide:circle-check" />
</ElButton>
</ElRow>
</div>
</div>
<!-- 分页组件 -->
<ElPagination
background
layout="prev, pager, next, sizes, total"
:total="imageTotal"
v-model:current-page="imageCurrentPage"
v-model:page-size="imagePageSize"
class="mt-4"
/>
</template>
</ImageGrid>
</div>
<!-- 类型voice -->
<div v-else-if="props.type === 'voice'">
@@ -349,37 +508,50 @@ watch(
</VideoGrid>
</div>
<!-- 类型news -->
<div v-else-if="props.type === 'news'">
<div
class="mx-auto w-full columns-1 [column-gap:10px] md:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5"
v-loading="loading"
>
<div
class="mb-2.5 break-inside-avoid border border-[#eaeaea] p-2.5"
v-for="item in list"
:key="item.mediaId"
>
<div v-if="item.content && item.content.newsItem">
<WxNews :articles="item.content.newsItem" />
<ElRow class="flex justify-center pt-2.5">
<ElButton type="success" @click="selectMaterialFun(item)">
选择
<IconifyIcon icon="lucide:circle-check" />
</ElButton>
</ElRow>
<div v-else-if="props.type === 'news'" class="news-grid-wrapper">
<NewsGrid>
<template #default>
<div
class="mx-auto w-full columns-1 [column-gap:10px] md:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5"
v-loading="newsLoading"
>
<div
class="mb-2.5 break-inside-avoid border border-[#eaeaea] p-2.5"
v-for="item in newsList"
:key="item.mediaId"
>
<div v-if="item.content && item.content.newsItem">
<WxNews :articles="item.content.newsItem" />
<ElRow class="flex justify-center pt-2.5">
<ElButton type="success" @click="selectMaterialFun(item)">
选择
<IconifyIcon icon="lucide:circle-check" />
</ElButton>
</ElRow>
</div>
</div>
</div>
</div>
</div>
<!-- 分页组件 -->
<ElPagination
background
layout="prev, pager, next, sizes, total"
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@current-change="getMaterialPageFun"
@size-change="getMaterialPageFun"
/>
<!-- 分页组件 -->
<ElPagination
background
layout="prev, pager, next, sizes, total"
:total="newsTotal"
v-model:current-page="newsCurrentPage"
v-model:page-size="newsPageSize"
class="mt-4"
/>
</template>
</NewsGrid>
</div>
</div>
</template>
<style scoped>
.image-grid-wrapper :deep(.vxe-grid--body-wrapper) {
display: none;
}
.news-grid-wrapper :deep(.vxe-grid--body-wrapper) {
display: none;
}
</style>

View File

@@ -1,5 +1,8 @@
<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 { computed, provide, ref } from 'vue';
import { useAccess } from '@vben/access';
import { confirm, DocAlert, Page } from '@vben/common-ui';
@@ -7,83 +10,117 @@ import { IconifyIcon } from '@vben/icons';
import {
ElButton,
ElCard,
ElForm,
ElFormItem,
ElLoading,
ElMessage,
ElPagination,
ElTabPane,
ElTabs,
} from 'element-plus';
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 './modules/image-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';
import VideoTable from './modules/video-table.vue';
import VoiceTable from './modules/voice-table.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);
// TODO @dlyan @AI这里是不是应该都用 grid类似 yudao-ui-admin-vben-v5/apps/web-ele/src/views/mp/autoReply/index.vue
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,
height: type.value === UploadType.Image ? 220 : 'auto',
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MpMaterialApi.Material>,
});
// 当 tab 切换时,更新 Grid 的 columns 和 rowConfig
async function onTabChange() {
const columns = getColumnsByType();
await gridApi.setGridOptions({
columns,
rowConfig: {
keyField: 'id',
isHover: true,
height: type.value === UploadType.Image ? 220 : 'auto',
},
});
await gridApi.reload();
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.pageNo = 1;
getList();
async function handleAccountChange(id: number) {
accountId.value = id;
// 同步设置表单值
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();
}
/** 处理删除操作 */
@@ -96,24 +133,11 @@ async function handleDelete(id: number) {
try {
await deletePermanentMaterial(id);
ElMessage.success('删除成功');
await getList();
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 分页改变事件 */
function handlePageChange(page: number) {
queryParams.pageNo = page;
getList();
}
/** 每页条数改变事件 */
function handleSizeChange(pageSize: number) {
queryParams.pageSize = pageSize;
queryParams.pageNo = 1;
getList();
}
</script>
<template>
@@ -121,18 +145,12 @@ function handleSizeChange(pageSize: number) {
<template #doc>
<DocAlert title="公众号素材" url="https://doc.iocoder.cn/mp/material/" />
</template>
<div class="h-full">
<!-- 搜索工作栏 -->
<ElCard class="h-[10%]" shadow="never">
<ElForm :model="queryParams" :inline="true">
<ElFormItem label="公众号" class="w-52">
<WxAccountSelect @change="onAccountChanged" />
</ElFormItem>
</ElForm>
</ElCard>
<ElCard shadow="never" class="mt-4 h-auto">
<ElTabs v-model="type" @tab-change="onTabChange">
<Grid class="material-grid">
<template #form-accountId>
<WxAccountSelect @change="handleAccountChange" />
</template>
<template #toolbar-actions>
<ElTabs v-model="type" class="w-full" @tab-change="onTabChange">
<!-- tab 1图片 -->
<ElTabPane :name="UploadType.Image">
<template #label>
@@ -141,34 +159,6 @@ function handleSizeChange(pageSize: number) {
图片
</span>
</template>
<!-- 列表 -->
<ImageTable
:key="`image-${type}`"
:list="list"
:loading="loading"
@delete="handleDelete"
@refresh="getList"
>
<template #toolbar-tools>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Image"
@uploaded="getList"
/>
</template>
</ImageTable>
<!-- 分页组件 -->
<div 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"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</ElTabPane>
<!-- tab 2语音 -->
@@ -179,34 +169,6 @@ function handleSizeChange(pageSize: number) {
语音
</span>
</template>
<!-- 列表 -->
<VoiceTable
:key="`voice-${type}`"
:list="list"
:loading="loading"
@delete="handleDelete"
@refresh="getList"
>
<template #toolbar-tools>
<UploadFile
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
:type="UploadType.Voice"
@uploaded="getList"
/>
</template>
</VoiceTable>
<!-- 分页组件 -->
<div 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"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</ElTabPane>
<!-- tab 3视频 -->
@@ -217,41 +179,80 @@ function handleSizeChange(pageSize: number) {
视频
</span>
</template>
<!-- 列表 -->
<VideoTable
:key="`video-${type}`"
:list="list"
:loading="loading"
@delete="handleDelete"
@refresh="getList"
>
<template #toolbar-tools>
<ElButton
v-if="hasAccessByCodes(['mp:material:upload-permanent'])"
type="primary"
@click="showCreateVideo = true"
>
新建视频
</ElButton>
</template>
</VideoTable>
<!-- 新建视频的弹窗 -->
<UploadVideo v-model:open="showCreateVideo" @uploaded="getList" />
<!-- 分页组件 -->
<div 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"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</ElTabPane>
</ElTabs>
</ElCard>
</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"
/>
<ElButton
v-if="
hasAccessByCodes(['mp:material:upload-permanent']) &&
type === UploadType.Video
"
type="primary"
@click="showCreateVideo = true"
>
新建视频
</ElButton>
</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: '删除',
icon: ACTION_ICON.delete,
color: 'danger',
auth: ['mp:material:delete'],
onClick: () => handleDelete(row.id),
},
]"
/>
</template>
</Grid>
<!-- 新建视频的弹窗 -->
<UploadVideo v-model:open="showCreateVideo" @uploaded="handleRefresh" />
</Page>
</template>

View File

@@ -1,3 +1,4 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
@@ -131,3 +132,14 @@ export function useImageGridColumns(): VxeTableGridOptions<MpMaterialApi.Materia
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'accountId',
label: '公众号',
component: 'Input',
},
];
}

View File

@@ -1,124 +0,0 @@
<script lang="ts" setup>
import type { MpMaterialApi } from '#/api/mp/material';
import { nextTick, onMounted, watch } from 'vue';
import { $t } from '@vben/locales';
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];
refresh: [];
}>();
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,
},
toolbarConfig: {
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,
},
},
});
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);
},
{ immediate: true, 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 #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<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: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该图片吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -1,121 +0,0 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
import { nextTick, watch } from 'vue';
import { $t } from '@vben/locales';
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];
refresh: [];
}>();
const columns = useVideoGridColumns();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns,
keepSource: true,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
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,
async (list: MpMaterialApi.Material[]) => {
const data = Array.isArray(list) ? list : [];
await nextTick();
updateGridData(data);
},
{ immediate: true, flush: 'post' },
);
watch(
() => props.loading,
(loading: boolean) => {
gridApi.setLoading(loading);
},
{ immediate: true },
);
</script>
<template>
<Grid class="mt-4">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<template #video="{ row }">
<WxVideoPlayer v-if="row.url" :url="row.url" />
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '下载',
type: 'primary',
link: true,
icon: ACTION_ICON.DOWNLOAD,
onClick: () => openWindow(row.url),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该视频吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -1,121 +0,0 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
import { nextTick, watch } from 'vue';
import { $t } from '@vben/locales';
import { openWindow } from '@vben/utils';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { WxVoicePlayer } from '#/views/mp/components';
import { useVoiceGridColumns } from './data';
const props = defineProps<{
list: MpMaterialApi.Material[];
loading: boolean;
}>();
const emit = defineEmits<{
delete: [v: number];
refresh: [];
}>();
const columns = useVoiceGridColumns();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns,
keepSource: true,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
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,
async (list: MpMaterialApi.Material[]) => {
const data = Array.isArray(list) ? list : [];
await nextTick();
updateGridData(data);
},
{ immediate: true, flush: 'post' },
);
watch(
() => props.loading,
(loading: boolean) => {
gridApi.setLoading(loading);
},
{ immediate: true },
);
</script>
<template>
<Grid class="mt-4">
<template #toolbar-tools>
<slot name="toolbar-tools"></slot>
</template>
<template #voice="{ row }">
<WxVoicePlayer v-if="row.url" :url="row.url" />
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '下载',
type: 'primary',
link: true,
icon: ACTION_ICON.DOWNLOAD,
onClick: () => openWindow(row.url),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mp:material:delete'],
popConfirm: {
title: '确定要删除该语音吗?',
confirm: () => emit('delete', row.id!),
},
},
]"
/>
</template>
</Grid>
</template>