feat: 自动回复迁移

This commit is contained in:
hw
2025-11-04 16:53:08 +08:00
parent 7a5f4b01e2
commit 84795d10cd
28 changed files with 2060 additions and 103 deletions

View File

@@ -1,23 +1,26 @@
<script lang="ts" setup>
import type { MpAccountApi } from '#/api/mp/account';
import { onMounted, reactive, ref, unref } from 'vue';
import { computed, onMounted, reactive, ref, unref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useTabs } from '@vben/hooks';
import { message, Select } from 'ant-design-vue';
import { message, Select, SelectOption } from 'ant-design-vue';
import { getSimpleAccountList } from '#/api/mp/account';
import { useTagsViewStore } from '#/store/tagsView';
defineOptions({ name: 'WxAccountSelect' });
const emit = defineEmits<{
(e: 'change', id: number, name: string): void;
const props = defineProps<{
modelValue?: number;
}>();
// 消息弹窗
const { closeCurrentTab } = useTabs(); // 视图操作
const emit = defineEmits<{
(e: 'change', id: number, name: string): void;
(e: 'update:modelValue', id: number): void;
}>();
const { delView } = useTagsViewStore(); // 视图操作
const { push, currentRoute } = useRouter();
const account: MpAccountApi.AccountSimple = reactive({
@@ -27,37 +30,78 @@ const account: MpAccountApi.AccountSimple = reactive({
const accountList = ref<MpAccountApi.AccountSimple[]>([]);
// 计算当前选中的 ID优先使用 modelValue表单绑定否则使用内部 account.id
const currentId = computed({
get: () => {
// 如果外部传入了 modelValue优先使用外部的值
if (props.modelValue !== undefined && props.modelValue !== null) {
return props.modelValue;
}
return account.id;
},
set: (value: number) => {
// 更新内部状态
account.id = value;
// 同步到外部(表单系统)
emit('update:modelValue', value);
// 触发 change 事件(保持向后兼容)
const found = accountList.value.find(
(v: MpAccountApi.AccountSimple) => v.id === value,
);
if (found) {
account.name = found.name;
emit('change', value, found.name);
}
},
});
// 监听外部 modelValue 变化,同步到内部状态
watch(
() => props.modelValue,
(newValue) => {
if (
newValue !== undefined &&
newValue !== null &&
newValue !== account.id
) {
account.id = newValue;
const found = accountList.value.find(
(v: MpAccountApi.AccountSimple) => v.id === newValue,
);
if (found) {
account.name = found.name;
}
}
},
);
/** 查询公众号列表 */
async function handleQuery() {
accountList.value = await getSimpleAccountList();
if (accountList.value.length === 0) {
message.error('未配置公众号,请在【公众号管理 -> 账号管理】菜单,进行配置');
await closeCurrentTab(unref(currentRoute));
delView(unref(currentRoute));
await push({ name: 'MpAccount' });
return;
}
// 默认选中第一个
const firstAccount = accountList.value[0];
if (firstAccount) {
account.id = firstAccount.id;
if (account.id) {
account.name = firstAccount.name;
emit('change', account.id, account.name);
}
}
}
/** 公众号变化 */
function onChanged(value: any) {
if (value === undefined || Array.isArray(value)) return;
const id = typeof value === 'number' ? value : Number(value);
account.id = id;
const found = accountList.value.find(
(v: MpAccountApi.AccountSimple) => v.id === id,
);
if (account.id && found) {
account.name = found.name;
emit('change', account.id, account.name);
// 如果外部没有传入值modelValue 为空),默认选中第一个
if (props.modelValue === undefined || props.modelValue === null) {
const firstAccount = accountList.value[0];
if (firstAccount) {
currentId.value = firstAccount.id;
account.name = firstAccount.name;
emit('change', firstAccount.id, firstAccount.name);
}
} else {
// 如果外部有值,同步到内部状态
const found = accountList.value.find(
(v: MpAccountApi.AccountSimple) => v.id === props.modelValue,
);
if (found) {
account.id = props.modelValue;
account.name = found.name;
}
}
}
@@ -69,23 +113,12 @@ onMounted(() => {
<template>
<Select
v-model:value="account.id"
v-model:value="currentId"
placeholder="请选择公众号"
class="!w-240px"
@change="onChanged"
style="width: 240px"
>
<Select.Option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
>
<SelectOption v-for="item in accountList" :key="item.id" :value="item.id">
{{ item.name }}
</Select.Option>
</SelectOption>
</Select>
</template>
<style lang="scss" scoped>
:deep(.ant-select-selector) {
width: 240px !important;
}
</style>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { UploadFile } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { Reply } from './types';
@@ -54,9 +55,65 @@ function beforeVideoUpload(file: UploadFile) {
return useBeforeUpload(UploadType.Video, 10)(file as any);
}
/** 自定义上传请求 */
async function customRequest(info: UploadRequestOption) {
const formData = new FormData();
formData.append('file', info.file as File);
formData.append('accountId', String(uploadData.accountId));
formData.append('type', uploadData.type);
if (uploadData.title) {
formData.append('title', uploadData.title);
}
if (uploadData.introduction) {
formData.append('introduction', uploadData.introduction);
}
try {
const xhr = new XMLHttpRequest();
// 监听上传进度
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
info.onProgress?.({ percent });
}
});
// 监听上传完成
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const res = JSON.parse(xhr.responseText);
onUploadSuccess(res);
info.onSuccess?.(res);
} catch {
info.onError?.(new Error('解析响应失败'));
message.error('上传失败:解析响应失败');
}
} else {
info.onError?.(new Error(`上传失败HTTP ${xhr.status}`));
message.error('上传失败,请重试');
}
});
// 监听上传错误
xhr.addEventListener('error', () => {
info.onError?.(new Error('上传请求失败'));
message.error('上传失败,请重试');
});
// 发送请求
xhr.open('POST', UPLOAD_URL);
xhr.setRequestHeader('Authorization', HEADERS.Authorization);
xhr.send(formData);
} catch (error: any) {
info.onError?.(error);
message.error('上传失败,请重试');
}
}
/** 上传成功 */
function onUploadSuccess(info: any) {
const res = info.response || info;
function onUploadSuccess(res: any) {
if (res.code !== 0) {
message.error(`上传出错:${res.msg}`);
return false;
@@ -66,7 +123,6 @@ function onUploadSuccess(info: any) {
fileList.value = [];
uploadData.title = '';
uploadData.introduction = '';
selectMaterial(res.data);
}
@@ -127,18 +183,9 @@ function selectMaterial(item: any) {
<!-- 文件上传 -->
<Col :span="12">
<Upload
:action="UPLOAD_URL"
:headers="HEADERS"
:file-list="fileList"
:data="uploadData"
:before-upload="beforeVideoUpload"
@change="
(info) => {
if (info.file.status === 'done') {
onUploadSuccess(info.file.response || info.file);
}
}
"
:custom-request="customRequest"
>
<Button type="primary">
新建视频 <IconifyIcon icon="ep:upload" />

View File

@@ -13,11 +13,12 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { VideoPlayer } from '@videojs-player/vue';
import { Modal } from 'ant-design-vue';
// import { VideoPlayer } from '@videojs-player/vue';
// import 'video.js/dist/video-js.css';
import 'video.js/dist/video-js.css';
defineOptions({ name: 'WxVideoPlayer' });
@@ -42,19 +43,23 @@ const playVideo = () => {
<template>
<div @click="playVideo()">
<!-- 提示 -->
<div>
<Icon icon="ep:video-play" :size="32" class="mr-5px" />
<div class="flex cursor-pointer flex-col items-center">
<IconifyIcon icon="ep:video-play" class="size-5" />
<p class="text-sm">点击播放视频</p>
</div>
<!-- 弹窗播放 -->
<Modal v-model:open="dialogVideo" title="视频播放" width="800px">
<Modal
v-model:open="dialogVideo"
title="视频播放"
width="900px"
:footer="null"
>
<VideoPlayer
v-if="dialogVideo"
class="video-player vjs-big-play-centered"
:src="props.url"
poster=""
crossorigin="anonymous"
controls
playsinline
:volume="0.6"