fix: resolve todo

This commit is contained in:
dylanmay
2025-12-15 10:13:13 +08:00
parent 380f74015e
commit 12b0575ca1
12 changed files with 991 additions and 1326 deletions

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>