mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
217 lines
6.7 KiB
TypeScript
217 lines
6.7 KiB
TypeScript
import { ref, computed } from 'vue';
|
|
import axios from 'axios';
|
|
|
|
export interface StagedFileInfo {
|
|
attachment_id: string;
|
|
filename: string;
|
|
original_name: string;
|
|
url: string; // blob URL for preview
|
|
type: string; // image, record, file, video
|
|
signature?: string;
|
|
}
|
|
|
|
export function useMediaHandling() {
|
|
const stagedFiles = ref<StagedFileInfo[]>([]);
|
|
const mediaCache = ref<Record<string, string>>({});
|
|
const pendingFileSignatures = new Set<string>();
|
|
|
|
async function getFileSignature(file: File): Promise<string> {
|
|
if (crypto?.subtle) {
|
|
const buffer = await file.arrayBuffer();
|
|
const digest = await crypto.subtle.digest('SHA-256', buffer);
|
|
const hash = Array.from(new Uint8Array(digest))
|
|
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
return `sha256:${hash}`;
|
|
}
|
|
|
|
return `meta:${file.name}:${file.size}:${file.type}:${file.lastModified}`;
|
|
}
|
|
|
|
function isDuplicateFile(signature: string) {
|
|
return (
|
|
pendingFileSignatures.has(signature) ||
|
|
stagedFiles.value.some(file => file.signature === signature)
|
|
);
|
|
}
|
|
|
|
async function getMediaFile(filename: string): Promise<string> {
|
|
if (mediaCache.value[filename]) {
|
|
return mediaCache.value[filename];
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get('/api/chat/get_file', {
|
|
params: { filename },
|
|
responseType: 'blob'
|
|
});
|
|
|
|
const blobUrl = URL.createObjectURL(response.data);
|
|
mediaCache.value[filename] = blobUrl;
|
|
return blobUrl;
|
|
} catch (error) {
|
|
console.error('Error fetching media file:', error);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
async function uploadStagedFile(file: File): Promise<StagedFileInfo | undefined> {
|
|
const signature = await getFileSignature(file);
|
|
if (isDuplicateFile(signature)) return undefined;
|
|
|
|
pendingFileSignatures.add(signature);
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const response = await axios.post('/api/chat/post_file', formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
}
|
|
});
|
|
|
|
const { attachment_id, filename, type } = response.data.data;
|
|
const stagedFile = {
|
|
attachment_id,
|
|
filename,
|
|
original_name: file.name,
|
|
url: URL.createObjectURL(file),
|
|
type,
|
|
signature
|
|
};
|
|
stagedFiles.value.push(stagedFile);
|
|
return stagedFile;
|
|
} catch (err) {
|
|
console.error('Error uploading file:', err);
|
|
return undefined;
|
|
} finally {
|
|
pendingFileSignatures.delete(signature);
|
|
}
|
|
}
|
|
|
|
async function processAndUploadImage(file: File) {
|
|
return uploadStagedFile(file);
|
|
}
|
|
|
|
async function processAndUploadFile(file: File) {
|
|
return uploadStagedFile(file);
|
|
}
|
|
|
|
async function handlePaste(event: ClipboardEvent) {
|
|
const items = event.clipboardData?.items;
|
|
if (!items) return;
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
if (items[i].type.indexOf('image') !== -1) {
|
|
const file = items[i].getAsFile();
|
|
if (file) {
|
|
await processAndUploadImage(file);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeImage(index: number) {
|
|
// 找到第 index 个图片类型的文件
|
|
let imageCount = 0;
|
|
for (let i = 0; i < stagedFiles.value.length; i++) {
|
|
if (stagedFiles.value[i].type === 'image') {
|
|
if (imageCount === index) {
|
|
const fileToRemove = stagedFiles.value[i];
|
|
if (fileToRemove.url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(fileToRemove.url);
|
|
}
|
|
stagedFiles.value.splice(i, 1);
|
|
return;
|
|
}
|
|
imageCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeAudio() {
|
|
for (let i = stagedFiles.value.length - 1; i >= 0; i--) {
|
|
if (stagedFiles.value[i].type !== 'record') continue;
|
|
|
|
const fileToRemove = stagedFiles.value[i];
|
|
if (fileToRemove.url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(fileToRemove.url);
|
|
}
|
|
stagedFiles.value.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
function removeFile(index: number) {
|
|
// Find the requested non-image, non-audio attachment.
|
|
let fileCount = 0;
|
|
for (let i = 0; i < stagedFiles.value.length; i++) {
|
|
if (
|
|
stagedFiles.value[i].type !== 'image' &&
|
|
stagedFiles.value[i].type !== 'record'
|
|
) {
|
|
if (fileCount === index) {
|
|
const fileToRemove = stagedFiles.value[i];
|
|
if (fileToRemove.url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(fileToRemove.url);
|
|
}
|
|
stagedFiles.value.splice(i, 1);
|
|
return;
|
|
}
|
|
fileCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearStaged(options: { revokeUrls?: boolean } = {}) {
|
|
const { revokeUrls = true } = options;
|
|
if (revokeUrls) {
|
|
// 清理文件的 blob URLs
|
|
stagedFiles.value.forEach(file => {
|
|
if (file.url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(file.url);
|
|
}
|
|
});
|
|
}
|
|
stagedFiles.value = [];
|
|
}
|
|
|
|
function cleanupMediaCache() {
|
|
Object.values(mediaCache.value).forEach(url => {
|
|
if (url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
});
|
|
mediaCache.value = {};
|
|
}
|
|
|
|
// 计算属性:获取图片的 URL 列表(用于预览)
|
|
const stagedImagesUrl = computed(() =>
|
|
stagedFiles.value.filter(f => f.type === 'image').map(f => f.url)
|
|
);
|
|
|
|
const stagedAudioUrl = computed(() =>
|
|
stagedFiles.value.find(f => f.type === 'record')?.url || ''
|
|
);
|
|
|
|
// 计算属性:获取非图片文件列表
|
|
const stagedNonImageFiles = computed(() =>
|
|
stagedFiles.value.filter(f => f.type !== 'image' && f.type !== 'record')
|
|
);
|
|
|
|
return {
|
|
stagedImagesUrl,
|
|
stagedAudioUrl,
|
|
stagedFiles,
|
|
stagedNonImageFiles,
|
|
getMediaFile,
|
|
processAndUploadImage,
|
|
processAndUploadFile,
|
|
handlePaste,
|
|
removeImage,
|
|
removeAudio,
|
|
removeFile,
|
|
clearStaged,
|
|
cleanupMediaCache
|
|
};
|
|
}
|