Files
reader-star/apps/admin/src/views/template/template-detail/util.ts
rjl c3d23224ba feat(admin): 新增模板详情页书籍编辑功能
- 新增模板详情页,支持导入PDF书籍、管理目录和页面
- 实现书籍页面布局编辑功能,可添加/编辑普通题目和特殊区域
- 添加题目管理功能,支持单选题、多选题、判断题等题型
- 集成OSS上传服务,支持题目图片裁剪和上传
- 实现事件总线机制,用于组件间通信
- 添加工具函数:路径拼接、版本比较等
- 更新环境配置,启用强制登录并调整API地址
2026-01-27 11:10:58 +08:00

139 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ref } from 'vue';
import pLimit from 'p-limit';
import { type BookPageQuestion, type QuestionsImages, updateBookPageLayoutAxios } from '@/service/api/book';
import { getAliOssTokenAxios } from '@/service/api/upload';
import { initOSSClient } from '@/utils/oss';
import type { CropImageOptions, CurrBookPageAllInfo, QuestionInfo } from './types';
import { compareVersion } from '@/utils/common';
// Crop image function implementation
const cropImage = async (url: string, client: any, options: CropImageOptions, id: number): Promise<QuestionsImages> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = url;
img.onload = async () => {
try {
const canvas = document.createElement('canvas');
canvas.width = options.width;
canvas.height = options.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Canvas context not available'));
return;
}
// Draw cropped image
ctx.drawImage(
img,
options.x,
options.y,
options.width,
options.height,
0,
0,
options.width,
options.height
);
// Convert to Blob
canvas.toBlob(async (blob) => {
if (!blob) {
reject(new Error('Canvas to Blob failed'));
return;
}
const file = new File([blob], `question_${id}_${Date.now()}.png`, { type: 'image/png' });
const path = `book/questions/${id}/${file.name}`;
try {
// Upload to OSS
// Assuming client.put or multipartUpload.
// Since initOSSClient returns an OSS client, we can use it.
// Using put for small files (cropped images are usually small)
const result = await client.put(path, file);
// Return the result format expected by the API
// The API expects a map of ID to URL? Or array of objects?
// Based on usage: `tasks.push(...)` and `imageFiles.push(...list)`
// And `updateBookPageLayoutAxios` takes `questionsImages: imageFiles`
// Let's return an object with the ID as key and URL as value, or just the URL object if QuestionsImages is a type.
// Looking at previous usage `urlObj[item.id!] = item`, it seems `QuestionsImages` might be `{ [id]: url }`.
resolve({ [id]: result.url });
} catch (err) {
reject(err);
}
}, 'image/png');
} catch (err) {
reject(err);
}
};
img.onerror = (err) => {
reject(new Error('Image load failed'));
};
});
};
/** 提交 */
export function usePageLayoutSubmit() {
const submmitLoading = ref(false);
async function pageLayoutSubmit(currBookPage: CurrBookPageAllInfo) {
try {
submmitLoading.value = true;
const urlObj: Record<number, BookPageQuestion> = {};
currBookPage!.currBookPageData.questions.forEach((item) => {
if (item.id) {
urlObj[item.id!] = item;
}
});
// 获取阿里云OSS STS
const aliOssSTS = await getAliOssTokenAxios();
const client = initOSSClient(aliOssSTS);
// 截取图片并上传 5个并发
const imageFiles: QuestionsImages[] = [];
const limit = pLimit(5);
const tasks: Promise<QuestionsImages>[] = [];
currBookPage!.currBookPageData.layout.forEach((item) => {
const question: QuestionInfo = item.question;
if (currBookPage!.currBookPageData.url) {
const cropImageOptions: CropImageOptions = { x: question.x, y: question.y, width: question.w, height: question.h };
const url = currBookPage!.currBookPageData.url;
//截取图片并添加到上传队列
tasks.push(limit(() => cropImage(url, client, cropImageOptions, item.questionId!)));
}
});
const list = await Promise.all(tasks);
imageFiles.push(...list);
currBookPage!.currBookPageData.layout = currBookPage!.currBookPageData.layout.sort((a, b) => {
return compareVersion(a.question.no, b.question.no, '-');
});
await updateBookPageLayoutAxios({
id: currBookPage!.currBookPageData!.bookPageId!,
layout: JSON.stringify(currBookPage!.currBookPageData.layout),
questionsImages: imageFiles,
});
submmitLoading.value = false;
return Promise.resolve();
} catch (error) {
console.log('error====', error);
submmitLoading.value = false;
return Promise.reject(error);
}
}
return {
pageLayoutSubmit,
submmitLoading,
};
}
export function getPDFPageWidth(width: number) {
return width;
}