From a9443035c2466806fc054b8f7629311f31eee56e Mon Sep 17 00:00:00 2001 From: zdg Date: Wed, 27 Nov 2024 17:58:25 +0800 Subject: [PATCH] =?UTF-8?q?ppt=E6=96=87=E4=BB=B6=E8=BD=AC=E5=85=A5?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/AixPPTist/src/hooks/useImport.ts | 414 +++++++++++++++++- src/renderer/src/api/apiService.js | 6 + .../src/api/education/entpcoursefile.js | 8 + src/renderer/src/utils/comm.js | 23 + .../views/teachingDesign/container/right.vue | 142 +++++- 5 files changed, 584 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/AixPPTist/src/hooks/useImport.ts b/src/renderer/src/AixPPTist/src/hooks/useImport.ts index e39c69d..d0e450f 100644 --- a/src/renderer/src/AixPPTist/src/hooks/useImport.ts +++ b/src/renderer/src/AixPPTist/src/hooks/useImport.ts @@ -28,12 +28,12 @@ const convertFontSizePtToPx = (html: string, ratio: number) => { }) } -export default () => { - const slidesStore = useSlidesStore() - const { theme } = storeToRefs(useSlidesStore()) +const slidesStore = useSlidesStore() +const { theme } = storeToRefs(useSlidesStore()) - const { addSlidesFromData } = useAddSlidesOrElements() - const { isEmptySlide } = useSlideHandler() +const { addSlidesFromData } = useAddSlidesOrElements() +const { isEmptySlide } = useSlideHandler() +export default () => { const exporting = ref(false) @@ -486,9 +486,413 @@ export default () => { reader.readAsArrayBuffer(file) } + + return { importSpecificFile, importPPTXFile, + PPTXFileToJson, exporting, } +} + + // 导入PPTX 返回 json +export const PPTXFileToJson = (data: File|ArrayBuffer) => { + return new Promise(async(resolve, reject) => { + if (!data) return + let fileArrayBuffer: ArrayBuffer = null + let resData = {} // 返回的数据 + + const shapeList: ShapePoolItem[] = [] + for (const item of SHAPE_LIST) { + shapeList.push(...item.children) + } + // 获取文件的 ArrayBuffer + const getArrayBuffer = () => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + resolve(reader.result) + } + reader.onerror = reject + reader.readAsArrayBuffer(data) + }) + } + if (data instanceof File) { // 文件 + fileArrayBuffer = await getArrayBuffer() + } else if (data instanceof ArrayBuffer) { // ArrayBuffer + fileArrayBuffer = data + } else { + throw new Error('Invalid data type') + } + + // 开始解析 + const json = await parse(fileArrayBuffer) + + const ratio = 96 / 72 + const width = json.size.width + + resData.def = json // 保留原始数据 + resData.width = width * ratio + resData.ratio = slidesStore.viewportRatio + + const slides: Slide[] = [] + for (const item of json.slides) { + const { type, value } = item.fill + let background: SlideBackground + if (type === 'image') { + background = { + type: 'image', + image: { + src: value.picBase64, + size: 'cover', + }, + } + } + else if (type === 'gradient') { + background = { + type: 'gradient', + gradient: { + type: 'linear', + colors: value.colors.map(item => ({ + ...item, + pos: parseInt(item.pos), + })), + rotate: value.rot, + }, + } + } + else { + background = { + type: 'solid', + color: value, + } + } + + const slide: Slide = { + id: nanoid(10), + elements: [], + background, + } + + const parseElements = (elements: Element[]) => { + for (const el of elements) { + const originWidth = el.width || 1 + const originHeight = el.height || 1 + const originLeft = el.left + const originTop = el.top + + el.width = el.width * ratio + el.height = el.height * ratio + el.left = el.left * ratio + el.top = el.top * ratio + + if (el.type === 'text') { + const textEl: PPTTextElement = { + type: 'text', + id: nanoid(10), + width: el.width, + height: el.height, + left: el.left, + top: el.top, + rotate: el.rotate, + defaultFontName: theme.value.fontName, + defaultColor: theme.value.fontColor, + content: convertFontSizePtToPx(el.content, ratio), + lineHeight: 1, + outline: { + color: el.borderColor, + width: el.borderWidth, + style: el.borderType, + }, + fill: el.fillColor, + vertical: el.isVertical, + } + if (el.shadow) { + textEl.shadow = { + h: el.shadow.h * ratio, + v: el.shadow.v * ratio, + blur: el.shadow.blur * ratio, + color: el.shadow.color, + } + } + slide.elements.push(textEl) + } + else if (el.type === 'image') { + slide.elements.push({ + type: 'image', + id: nanoid(10), + src: el.src, + width: el.width, + height: el.height, + left: el.left, + top: el.top, + fixedRatio: true, + rotate: el.rotate, + flipH: el.isFlipH, + flipV: el.isFlipV, + }) + } + else if (el.type === 'audio') { + slide.elements.push({ + type: 'audio', + id: nanoid(10), + src: el.blob, + width: el.width, + height: el.height, + left: el.left, + top: el.top, + rotate: 0, + fixedRatio: false, + color: theme.value.themeColor, + loop: false, + autoplay: false, + }) + } + else if (el.type === 'video') { + slide.elements.push({ + type: 'video', + id: nanoid(10), + src: (el.blob || el.src)!, + width: el.width, + height: el.height, + left: el.left, + top: el.top, + rotate: 0, + autoplay: false, + }) + } + else if (el.type === 'shape') { + if (el.shapType === 'line' || /Connector/.test(el.shapType)) { + const lineElement = parseLineElement(el) + slide.elements.push(lineElement) + } + else { + const shape = shapeList.find(item => item.pptxShapeType === el.shapType) + + const vAlignMap: { [key: string]: ShapeTextAlign } = { + 'mid': 'middle', + 'down': 'bottom', + 'up': 'top', + } + + const element: PPTShapeElement = { + type: 'shape', + id: nanoid(10), + width: el.width, + height: el.height, + left: el.left, + top: el.top, + viewBox: [200, 200], + path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z', + fill: el.fillColor || 'none', + fixedRatio: false, + rotate: el.rotate, + outline: { + color: el.borderColor, + width: el.borderWidth, + style: el.borderType, + }, + text: { + content: convertFontSizePtToPx(el.content, ratio), + defaultFontName: theme.value.fontName, + defaultColor: theme.value.fontColor, + align: vAlignMap[el.vAlign] || 'middle', + }, + flipH: el.isFlipH, + flipV: el.isFlipV, + } + if (el.shadow) { + element.shadow = { + h: el.shadow.h * ratio, + v: el.shadow.v * ratio, + blur: el.shadow.blur * ratio, + color: el.shadow.color, + } + } + + if (shape) { + element.path = shape.path + element.viewBox = shape.viewBox + + if (shape.pathFormula) { + element.pathFormula = shape.pathFormula + element.viewBox = [el.width, el.height] + + const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula] + if ('editable' in pathFormula && pathFormula.editable) { + element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue) + element.keypoints = pathFormula.defaultValue + } + else element.path = pathFormula.formula(el.width, el.height) + } + } + if (el.shapType === 'custom') { + if (el.path!.indexOf('NaN') !== -1) element.path = '' + else { + element.special = true + element.path = el.path! + + const { maxX, maxY } = getSvgPathRange(element.path) + element.viewBox = [maxX || originWidth, maxY || originHeight] + } + } + + if (element.path) slide.elements.push(element) + } + } + else if (el.type === 'table') { + const row = el.data.length + const col = el.data[0].length + + const style: TableCellStyle = { + fontname: theme.value.fontName, + color: theme.value.fontColor, + } + const data: TableCell[][] = [] + for (let i = 0; i < row; i++) { + const rowCells: TableCell[] = [] + for (let j = 0; j < col; j++) { + const cellData = el.data[i][j] + + let textDiv: HTMLDivElement | null = document.createElement('div') + textDiv.innerHTML = cellData.text + const p = textDiv.querySelector('p') + const align = p?.style.textAlign || 'left' + + const span = textDiv.querySelector('span') + const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : '' + const fontname = span?.style.fontFamily || '' + const color = span?.style.color || cellData.fontColor + + rowCells.push({ + id: nanoid(10), + colspan: cellData.colSpan || 1, + rowspan: cellData.rowSpan || 1, + text: textDiv.innerText, + style: { + ...style, + align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left', + fontsize, + fontname, + color, + bold: cellData.fontBold, + backcolor: cellData.fillColor, + }, + }) + textDiv = null + } + data.push(rowCells) + } + + const colWidths: number[] = new Array(col).fill(1 / col) + + slide.elements.push({ + type: 'table', + id: nanoid(10), + width: el.width, + height: el.height, + left: el.left, + top: el.top, + colWidths, + rotate: 0, + data, + outline: { + width: el.borderWidth || 2, + style: el.borderType, + color: el.borderColor || '#eeece1', + }, + cellMinHeight: 36, + }) + } + else if (el.type === 'chart') { + let labels: string[] + let legends: string[] + let series: number[][] + + if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') { + labels = el.data[0].map((item, index) => `坐标${index + 1}`) + legends = ['X', 'Y'] + series = el.data + } + else { + const data = el.data as ChartItem[] + labels = Object.values(data[0].xlabels) + legends = data.map(item => item.key) + series = data.map(item => item.values.map(v => v.y)) + } + + const options: ChartOptions = {} + + let chartType: ChartType = 'bar' + + switch (el.chartType) { + case 'barChart': + case 'bar3DChart': + chartType = 'bar' + if (el.barDir === 'bar') chartType = 'column' + if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true + break + case 'lineChart': + case 'line3DChart': + if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true + chartType = 'line' + break + case 'areaChart': + case 'area3DChart': + if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true + chartType = 'area' + break + case 'scatterChart': + case 'bubbleChart': + chartType = 'scatter' + break + case 'pieChart': + case 'pie3DChart': + chartType = 'pie' + break + case 'radarChart': + chartType = 'radar' + break + case 'doughnutChart': + chartType = 'ring' + break + default: + } + + slide.elements.push({ + type: 'chart', + id: nanoid(10), + chartType: chartType, + width: el.width, + height: el.height, + left: el.left, + top: el.top, + rotate: 0, + themeColors: [theme.value.themeColor], + textColor: theme.value.fontColor, + data: { + labels, + legends, + series, + }, + options, + }) + } + else if (el.type === 'group' || el.type === 'diagram') { + const elements = el.elements.map(_el => ({ + ..._el, + left: _el.left + originLeft, + top: _el.top + originTop, + })) + parseElements(elements) + } + } + } + parseElements(item.elements) + slides.push(slide) + } + resData.slides = slides + resolve(resData) + }) } \ No newline at end of file diff --git a/src/renderer/src/api/apiService.js b/src/renderer/src/api/apiService.js index b030354..99deb41 100644 --- a/src/renderer/src/api/apiService.js +++ b/src/renderer/src/api/apiService.js @@ -50,4 +50,10 @@ export class school { // 获取学校管理审核 static checkSchool = data => ApiService.publicHttp(`/smarttalk/audit/checkSchool`,data,'post') +} + +export class Other { + static baseUrl = "/common/upload" + // 测试 + static uploadFile = data => ApiService.publicHttp(this.baseUrl, data, 'post', null, 'file') } \ No newline at end of file diff --git a/src/renderer/src/api/education/entpcoursefile.js b/src/renderer/src/api/education/entpcoursefile.js index 26e2cf5..8b2052f 100644 --- a/src/renderer/src/api/education/entpcoursefile.js +++ b/src/renderer/src/api/education/entpcoursefile.js @@ -93,6 +93,14 @@ export function batchUpdateNew(data) { data: data }) } +// zdg: 批量新增pptist - 新 +export function batchAddNew(data) { + return request({ + url: '/education/entpcoursefile/batch/add', + method: 'post', + data: data + }) +} // 修改entpcoursefile export function updateFile2Redis(data) { diff --git a/src/renderer/src/utils/comm.js b/src/renderer/src/utils/comm.js index 1cbc5db..5c26eaf 100644 --- a/src/renderer/src/utils/comm.js +++ b/src/renderer/src/utils/comm.js @@ -21,6 +21,29 @@ export function getFiles() { } return new Promise(cb) } +/** + * base64 转 blob + */ +export function base64ToBlob(base64Data) { + const contentType = base64Data?.match(/^data:([^;]+);base64,/)?.[1]||'image/png' + // 去除Base64编码数据中的前缀(如"data:image/png;base64,") + const byteCharacters = atob(base64Data.split(',')[1]); + const byteArrays = []; + for (let i = 0; i < byteCharacters.length; i++) { + byteArrays.push(byteCharacters.charCodeAt(i)); + } + return new Blob([new Uint8Array(byteArrays)], { type: contentType }); +} + +export function arrayBufferToBlob(arrayBuffer, contentType) { + return new Blob([new Uint8Array(arrayBuffer)], { type: contentType }); +} + +export function blobToFile(blob, fileName, contentType) { + fileName = fileName || 'file' + contentType = contentType || blob.type ||'image/png' + return new File([blob], fileName, { type: contentType }); +} // ============= 数学公式--相关 =================== /** diff --git a/src/renderer/src/views/teachingDesign/container/right.vue b/src/renderer/src/views/teachingDesign/container/right.vue index 2be6c0d..5089b15 100644 --- a/src/renderer/src/views/teachingDesign/container/right.vue +++ b/src/renderer/src/views/teachingDesign/container/right.vue @@ -59,11 +59,23 @@ import emitter from '@/utils/mitt' import EditDialog from './edit-dialog.vue' import AdjustDialog from './adjust-dialog.vue' import { completion, tempResult } from '@/api/mode/index.js' -import { dataSetJson } from '@/utils/comm.js' +// import { dataSetJson } from '@/utils/comm.js' +import * as commUtils from '@/utils/comm.js' import PptDialog from '@/views/prepare/container/pptist-dialog.vue' +import useUserStore from '@/store/modules/user' +import {PPTXFileToJson} from '@/AixPPTist/src/hooks/useImport' // ppt转json +import * as API_entpcourse from '@/api/education/entpcourse' // 相关api +import * as API_entpcoursefile from '@/api/education/entpcoursefile' // 相关api +import * as Api_server from '@/api/apiService' // 相关api + +const userStore = useUserStore() const pptDialog = ref(false) const resultList = ref([]) +const courseObj = reactive({ + node: null, // 选择的课程节点 +}) + emitter.on('changeMode', (item) => { console.log(item, 'item') resultList.value = item.child @@ -98,9 +110,39 @@ const params = reactive( } ) -const addAiPPT = (res) => { +const addAiPPT = async(res) => { + let node = courseObj.node + if (!node) return msgUtils.msgWarning('请选择章节?') //TODO res中有PPT地址 - console.log(res) + const params = { evalid: node.id, edituserid: userStore.id, pageSize: 1 } + const resEnpt = await HTTP_SERVER_API('getCourseList', params) + if (!(resEnpt?.rows?.[0] || null)) { // 创建 + const resid = await HTTP_SERVER_API('addEntpcourse') + courseObj.entp.id = resid + } else courseObj.entp = resEnpt?.rows?.[0] || null + // 下载PPT 并解析json转换到我们自己数据库 + fetch(res.url) + .then(res => res.arrayBuffer()) + .then(async buffer => { + const resPptJson = await PPTXFileToJson(buffer) + const { def, slides, ...content } = resPptJson + console.log(slides) + for( let o of slides ) { + await toRousrceUrl(o) + } + // return + // 生成ppt课件-父级 + const p_params = {parentContent: JSON.stringify(content)} + const parentid = await HTTP_SERVER_API('addEntpcoursefile', p_params) + if (!!parentid??null) { // 生成内容幻灯片 + if (slides.length > 0) { + const resSlides = slides.map(({id, ...slide}) => slide) + const params = {parentid, filetype: 'slide', title: '', slides: resSlides } + const res_3 = await HTTP_SERVER_API('batchAddNew', params) + console.log('xxxx', res_3) + } + } + }) } const conversation = async () => { for (let item of resultList.value) { @@ -155,15 +197,107 @@ emitter.on('changeResult', (item) => { resultList.value[curIndex.value].answer = item }) +// ======== zdg start ============ +// 统一HTTP处理 +const HTTP_SERVER_API = (type, params = {}) => { + switch (type) { + case 'addEntpcourse': { // 添加课程 + const node = courseObj.node || {} + if (!node) return msgUtils.msgWarning('请选择章节?') + const def = { // 默认参数 + entpid: userStore.user.deptId, // 部门id + level: 1, // 层级 + parentid: 0, // 父级id + dictid: 0, // 字典id + evalid: node.id, // 章节id + evalparentid: node.parentid, // 单元id(父级id) + edusubject: node.edusubject, // 学科 + edudegree: node.edudegree, // 年级 + edustage: node.edustage, // 阶段 + coursetype: '课标学科', // 课程类型 + coursetitle: node.itemtitle, // 课程名称 + coursedesc: '', // 课程描述 + status: '', // 状态 + dflag: 0, // 状态 + edituserid: userStore.id, // 编辑人id + createblankfile: 'no', // 创建空白文件 + } + courseObj.entp = def + return API_entpcourse.addEntpcourse(def) + } + case 'addEntpcoursefile': { // 添加课程文件 + params = getDefParams(params) + return API_entpcoursefile.addEntpcoursefileReturnId(params) + } + case 'batchAddNew': { // 批量添加课程文件 + params = getDefParams(params) + return API_entpcoursefile.batchAddNew(params) + } + case 'getCourseList': { // 获取课程列表 + return API_entpcourse.listEntpcourse(params) + } + case 'getCourseFileList':{ // 获取课程文件列表 + return API_entpcoursefile.listEntpcoursefileNew(params) + } + } +} +// 获取默认参数 +const getDefParams = (params) => { + const enpt = courseObj.entp + const def = { + parentid: 0, + entpid: userStore.user.deptId, + entpcourseid: enpt.id, + ppttype: 'file', + title: enpt.coursetitle, + fileurl: '', + filetype: 'aptist', + datacontent: '', + filekey: '', + filetag: '', + fileidx: 0, + dflag: 0, + status: '', + edituserid: userStore.id + } + return Object.assign(def, params) +} +// 图片|音频|视频 转换为在线地址 +const toRousrceUrl = async(o) => { + if (!!o.src) { // 如果有src就转换 + const isBase64 = /^data:image\/(\w+);base64,/.test(o.src) + const isBlobUrl = /^blob:/.test(o.src) + console.log('isBase64', o, isBase64) + if (isBase64) { + const bolb = commUtils.base64ToBlob(o.src) + const fileName = Date.now() + '.png' + const file = commUtils.blobToFile(bolb, fileName) + // o.src = fileName + // console.log('file', file) + const formData = new FormData() + formData.append('file', file) + const res = await Api_server.Other.uploadFile(formData) + if (res && res.code == 200){ + const url = res?.url + url &&(o.src = url) + } + } else if (isBlobUrl) { // 视频和音频 + } + } + if (o?.background?.image) await toRousrceUrl(o.background.image) + if (o?.elements) o.elements.forEach(async o => {await toRousrceUrl(o)}) +} +// ======== zdg end ============ const curNode = reactive({}) onMounted(() => { let data = sessionStore.get('subject.curNode') Object.assign(curNode, data); + courseObj.node = data let jsonKey = `课标-${data.edustage}-${data.edusubject}` - params.dataset_id = dataSetJson[jsonKey] + params.dataset_id = commUtils.dataSetJson[jsonKey] })