ppt文件转入数据

This commit is contained in:
zdg 2024-11-27 17:58:25 +08:00
parent 7c3f3ea8fb
commit a9443035c2
5 changed files with 584 additions and 9 deletions

View File

@ -28,12 +28,12 @@ const convertFontSizePtToPx = (html: string, ratio: number) => {
})
}
export default () => {
const slidesStore = useSlidesStore()
const { theme } = storeToRefs(useSlidesStore())
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)
})
}

View File

@ -51,3 +51,9 @@ 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')
}

View File

@ -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) {

View File

@ -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 });
}
// ============= 数学公式--相关 ===================
/**

View File

@ -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' // pptjson
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 resPPT
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]
})