| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833 |
- <template>
- <main ref="canvas" class="canvas" :style="computedCanvasStyle()">
- <template v-if="isEdit">
- <div v-for="item in lineList" :key="item[0]" class="group-line" :style="computedGroupLine(item)"></div>
- <span class="drag-line" data-row="-1"></span>
- <!-- 行 -->
- <template v-for="(row, i) in data.row_list">
- <div :key="row.row_id" class="row" :style="computedRowStyle(i)">
- <el-checkbox
- v-if="row?.row_id"
- v-model="rowCheckList[row.row_id]"
- :class="['row-checkbox', `${row.row_id}`]"
- />
- <!-- 列 -->
- <template v-for="(col, j) in row.col_list">
- <span
- v-if="j === 0"
- :key="`start-${row.row_id}-${col.col_id}`"
- class="drag-vertical-line col-start"
- :data-row="i"
- :data-col="j"
- ></span>
- <div :key="col.col_id" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
- <!-- 网格 -->
- <template v-for="(grid, k) in col.grid_list">
- <span
- v-if="k === 0"
- :key="`start-${grid.id}`"
- class="drag-line grid-line drag-row"
- :style="{ gridArea: 'grid-top' }"
- :data-row="i"
- :data-col="j"
- :data-grid="k"
- data-type="row"
- ></span>
- <span
- v-if="grid.row > 1 && grid.row !== col.grid_list[k - 1].row"
- :key="`middle-${grid.id}`"
- :style="{ gridArea: `middle-${grid.grid_area}` }"
- :data-row="i"
- :data-col="j"
- :data-grid="k"
- data-type="col-middle"
- class="drag-line grid-line"
- ></span>
- <span
- :key="`left-${grid.id}`"
- :style="{ gridArea: `left-${grid.grid_area}` }"
- :data-row="i"
- :data-col="j"
- :data-grid="k"
- data-type="col-left"
- class="drag-vertical-line grid-line grid-line-left"
- ></span>
- <component
- :is="componentList[grid.type]"
- :id="grid.id"
- ref="component"
- :key="`grid-${grid.id}`"
- :old-id="grid?.oldId"
- :class="[grid.id]"
- :data-row="i"
- :data-col="j"
- :data-grid="k"
- :data-view-order="computedGridViewOrder(grid.id)"
- :border-color="computedBorderColor(row.row_id)"
- :style="computedGridStyle(grid, row.row_id)"
- :component-move="componentMove(i, j, k)"
- @deleteComponent="deleteComponent"
- @showSetting="showSetting"
- @copyComponent="copyComponent"
- @changeData="changeData"
- />
- <span
- :key="`right-${grid.id}`"
- :style="{ gridArea: `right-${grid.grid_area}` }"
- :data-row="i"
- :data-col="j"
- :data-grid="k + 1"
- data-type="col-right"
- class="drag-vertical-line grid-line grid-line-right"
- ></span>
- <span
- v-if="k === col.grid_list.length - 1"
- :key="`end-${grid.id}`"
- class="drag-line grid-line drag-row"
- :style="{ gridArea: `grid-bottom` }"
- :data-row="i"
- :data-col="j"
- :data-grid="k + 1"
- data-type="row"
- ></span>
- </template>
- </div>
- <span
- :key="`end-${row.row_id}-${col.col_id}`"
- class="drag-vertical-line col-end"
- :data-row="i"
- :data-col="j + 1"
- ></span>
- </template>
- </div>
- <span v-if="i < data.row_list.length - 1" :key="`row-${row.row_id}`" class="drag-line" :data-row="i"></span>
- </template>
- <span class="drag-line" :data-row="data.row_list.length - 1"></span>
- </template>
- <PreviewEdit
- v-else
- ref="previewEdit"
- :courseware-id="courseware_id"
- :row-list="data.row_list"
- @computedMoveData="computedMoveData"
- />
- </main>
- </template>
- <script>
- import { getRandomNumber } from '@/utils/index';
- import { componentList } from '../../data/bookType';
- import {
- ContentSaveCoursewareContent,
- ContentGetCoursewareContent,
- GetBookUnifiedAttrib,
- ContentSaveCoursewareComponentContent,
- } from '@/api/book';
- import _ from 'lodash';
- import { unified_attrib } from '@/common/data';
- import PreviewEdit from './PreviewEdit.vue';
- export default {
- name: 'CreateCanvas',
- components: {
- PreviewEdit,
- },
- inject: ['getCurSettingId'],
- provide() {
- return {
- getBookUnifiedAttr: () => this.book_unified_attrib,
- getTitleList: () => this.title_list,
- getProjectResourcePopedom: () => this.projectResourcePopedom,
- moveComponentDragStart: this.dragStart,
- };
- },
- props: {
- isEdit: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- const { project_id } = this.$route.query;
- return {
- courseware_id: this.$route.params.courseware_id,
- project_id,
- data: {
- background_image_url: '',
- background_position: {
- width: 100,
- height: 100,
- top: 0,
- left: 0,
- },
- background: {},
- // 组件列表
- row_list: [],
- // 样式调整
- unified_attrib,
- },
- visibleBackground: false, // 背景设置弹窗
- rowCheckList: {}, // 行复选框列表
- content_group_row_list: [], // 行分组id列表
- gridBorderColorList: ['#3fc7cc', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#d863ff', '#724fff'], // 网格边框颜色列表
- curType: 'divider',
- curParams: {}, // 当前组件参数
- scrollInterval: null, // 滚动定时器
- componentList,
- curRow: -2,
- curCol: -1,
- curGrid: -1,
- gridInsertType: '', // 网格插入类型
- enterCanvas: false, // 是否进入画布
- // 拖拽状态
- drag: {
- clientX: 0,
- clientY: 0,
- dragging: false,
- },
- visibleFullTextSettings: false,
- book_unified_attrib: unified_attrib,
- title_list: [],
- curComponentId: '', // 当前选中组件 id
- projectResourcePopedom: [], // 当前编辑人员的项目资源权限
- };
- },
- computed: {
- lineList() {
- let arr = [];
- this.content_group_row_list.forEach(({ row_id, is_pre_same_group }, i) => {
- if (is_pre_same_group) {
- arr.push([this.content_group_row_list[i - 1].row_id, row_id]);
- }
- });
- return arr;
- },
- },
- watch: {
- drag: {
- handler(val) {
- if (val.dragging) {
- const dragging = document.querySelector('.canvas-dragging');
- dragging.style.left = `${val.clientX}px`;
- dragging.style.top = `${val.clientY}px`;
- }
- },
- deep: true,
- },
- enterCanvas: {
- handler(val) {
- if (val) return;
- if (!this.isEdit) return;
- const dragLineList = document.querySelectorAll('.drag-line');
- dragLineList.forEach((item) => {
- item.style.opacity = 0;
- });
- this.curRow = -2;
- const dragVerticalLineList = document.querySelectorAll('.drag-vertical-line');
- dragVerticalLineList.forEach((item) => {
- item.style.opacity = 0;
- });
- this.curCol = -1;
- this.curGrid = -1;
- this.gridInsertType = '';
- },
- },
- 'data.row_list': {
- handler(val) {
- // row_list 更改时判断是否有当前设置 id 的组件
- const curSettingId = this.getCurSettingId();
- const find = val.find((row) => {
- return row.col_list.find((col) => {
- return col.grid_list.find((grid) => grid.id === curSettingId);
- });
- });
- if (!find) {
- this.$emit('showSettingEmpty');
- }
- this.rowCheckList = Object.fromEntries(val.filter((row) => row?.row_id).map((row) => [row.row_id, false]));
- // 增加新添的行
- val.forEach(({ row_id }, i) => {
- let isHas = this.content_group_row_list.some((group) => group.row_id === row_id);
- if (!isHas) {
- this.content_group_row_list.splice(i, 0, { row_id, is_pre_same_group: false });
- [i - 1, i + 1].forEach((start) => {
- let step = start < i ? -1 : 1;
- for (let j = start; j >= 0 && j < this.content_group_row_list.length; j += step) {
- if (this.content_group_row_list[j].is_pre_same_group) {
- this.content_group_row_list[j].is_pre_same_group = false;
- } else {
- break;
- }
- }
- });
- }
- });
- // 过滤掉已经不存在的行,并将第一行的 is_pre_same_group 设置为 false
- this.content_group_row_list = this.content_group_row_list.filter((group) =>
- val.find((row) => row.row_id === group.row_id),
- );
- if (this.content_group_row_list[0]) {
- this.content_group_row_list[0].is_pre_same_group = false;
- }
- },
- immediate: true,
- },
- rowCheckList: {
- handler(val) {
- // 如果同时有两个选中,将选中的挑选出来,并将它们的状态变为 false
- let selectedRowID = [];
- Object.keys(val).forEach((key) => {
- if (val[key]) {
- selectedRowID.push(key);
- }
- });
- if (selectedRowID.length > 1) {
- selectedRowID.forEach((id) => {
- this.rowCheckList[id] = false;
- });
- } else {
- return false;
- }
- let selectIndex = selectedRowID
- .map((id) => this.content_group_row_list.findIndex(({ row_id }) => row_id === id))
- .sort((a, b) => a - b);
- // 只处理选中两行的情况
- if (selectIndex[1] - selectIndex[0] === 1) {
- // 相邻两行,切换分组状态
- this.content_group_row_list[selectIndex[1]].is_pre_same_group =
- !this.content_group_row_list[selectIndex[1]].is_pre_same_group;
- } else {
- // 非相邻,判断中间行是否都已分组
- const allGrouped = this.content_group_row_list
- .slice(selectIndex[0] + 1, selectIndex[1] + 1)
- .every((group) => group.is_pre_same_group);
- for (let i = selectIndex[0] + 1; i <= selectIndex[1]; i++) {
- this.content_group_row_list[i].is_pre_same_group = !allGrouped;
- }
- }
- },
- deep: true,
- },
- },
- created() {
- const loading = this.$loading({
- lock: true,
- text: '组件加载中...',
- spinner: 'el-icon-loading',
- });
- ContentGetCoursewareContent({ id: this.courseware_id })
- .then(({ content, content_group_row_list, title_list, my_project_resource_popedom }) => {
- if (content) {
- let parsedContent = JSON.parse(content);
- if (
- parsedContent &&
- Array.isArray(parsedContent.row_list) &&
- parsedContent.row_list.length > 0 &&
- !parsedContent.row_list[0]?.row_id
- ) {
- this.data = this.normalizeRowColIds(parsedContent);
- } else {
- this.data = parsedContent;
- }
- loading.close();
- }
- if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list);
- if (title_list) this.title_list = title_list || [];
- this.$watch(
- 'data',
- () => {
- this.changeData();
- },
- {
- deep: true,
- },
- );
- this.projectResourcePopedom = my_project_resource_popedom || [];
- })
- .finally(() => {
- loading.close();
- });
- this.getBookUnifiedAttr();
- },
- mounted() {
- document.addEventListener('mousemove', this.dragMove);
- document.addEventListener('mouseup', this.dragEnd);
- },
- beforeDestroy() {
- document.removeEventListener('mousemove', this.dragMove);
- document.removeEventListener('mouseup', this.dragEnd);
- },
- methods: {
- changeData() {
- this.$emit('changeData');
- },
- fullTextSettings(data) {
- this.$refs.component.forEach((item) => {
- item.updateProperty('view_pinyin', data.view_pinyin);
- item.updateProperty('pinyin_position', data.pinyin_position);
- item.updateRichTextProperty('fontFamily', data.font);
- item.updateRichTextProperty('fontSize', data.font_size);
- item.updateRichTextProperty('lineHeight', data.line_height);
- item.updateRichTextProperty('color', data.text_color);
- item.updateRichTextProperty('align', data.align);
- item.setUnifiedAttr(data);
- });
- this.data.unified_attrib = data;
- },
- getBookUnifiedAttr() {
- GetBookUnifiedAttrib({ book_id: this.project_id }).then(({ content }) => {
- if (content) {
- this.book_unified_attrib = JSON.parse(content);
- }
- });
- },
- /**
- * 保存课件内容
- * @param {string} type 类型
- */
- async saveCoursewareContent(type) {
- let isAllLoader = false;
- if (this.$refs?.component === undefined || this.$refs?.component.length === 0) {
- isAllLoader = true;
- } else if (this.isEdit) {
- isAllLoader = this.$refs?.component?.every((item) => item.property.isGetContent);
- } else {
- isAllLoader = this.$refs?.previewEdit?.$refs?.preview?.every((item) => item.loader);
- }
- if (!isAllLoader) {
- this.$message.warning('有组件内容未加载完成,请稍后再试');
- return false;
- }
- const loading = this.$loading({
- lock: true,
- text: '保存中...',
- spinner: 'el-icon-loading',
- background: 'rgba(0, 0, 0, 0.7)',
- });
- try {
- // 先等待所有子组件内容保存完成
- if (this.isEdit) {
- const comps = Array.isArray(this.$refs?.component)
- ? this.$refs.component
- : [this.$refs?.component].filter(Boolean);
- await Promise.all(comps.map((item) => item.saveCoursewareComponentContent()));
- }
- // 再收集并保存页面结构
- let component_id_list = this.data.row_list.flatMap((row) =>
- row.col_list.flatMap((col) => col.grid_list.map((grid) => grid.id)),
- );
- let groupIdList = _.cloneDeep(this.content_group_row_list);
- let groupList = [];
- // 通过判断 is_pre_same_group 将组合并
- for (let i = 0; i < groupIdList.length; i++) {
- if (groupIdList[i].is_pre_same_group) {
- groupList[groupList.length - 1].row_id_list.push(groupIdList[i].row_id);
- } else {
- groupList.push({
- name: '',
- row_id_list: [groupIdList[i].row_id],
- component_id_list: [],
- });
- }
- }
- // 通过合并后的分组,获取对应的组件 id 和分组名称
- groupList.forEach(({ row_id_list, component_id_list }, i) => {
- row_id_list.forEach((row_id, j) => {
- let row = this.data.row_list.find((row) => {
- return row.row_id === row_id;
- });
- // 当前行所有组件id列表
- let gridIdList = row.col_list.map((col) => col.grid_list.map((grid) => grid.id)).flat();
- component_id_list.push(...gridIdList);
- // 查找每组第一行中第一个包含 describe、label 或 stem 的组件
- if (j === 0) {
- let findKey = '';
- let findType = '';
- row.col_list.some((col) => {
- const findItem = col.grid_list.find(({ type }) => {
- return ['describe', 'label', 'stem'].includes(type);
- });
- if (findItem) {
- findKey = findItem.id;
- findType = findItem.type;
- return true;
- }
- });
- let groupName = `组${i + 1}`;
- // 如果有标签类组件,获取对应名称
- if (findKey) {
- let item = this.isEdit
- ? this.findChildComponentByKey(`grid-${findKey}`)
- : this.$refs.previewEdit.findChildComponentByKey(`preview-${findKey}`);
- if (['describe', 'stem'].includes(findType)) {
- groupName = item.data.content.replace(/<[^>]+>/g, '');
- } else if (findType === 'label') {
- groupName = item.data.dynamicTags.map((tag) => tag.text).join(', ');
- }
- }
- groupList[i].name = groupName;
- }
- });
- });
- await ContentSaveCoursewareContent({
- id: this.courseware_id,
- category: 'NEW',
- content: JSON.stringify(this.data),
- component_id_list,
- content_group_component_list: groupList,
- content_group_row_list: this.content_group_row_list,
- });
- this.$message.success('保存成功');
- if (type === 'quit') {
- this.$emit('back');
- }
- if (type === 'edit') {
- this.$emit('changeEditStatus');
- }
- } finally {
- loading.close();
- }
- },
- setBackground(url, position, background) {
- this.data.background_image_url = url;
- this.data.background_position = position;
- this.data.background = background;
- this.$message.success('设置背景图成功');
- },
- /**
- * 显示设置
- * @param {object} setting 组件设置数据
- * @param {string} type 组件类型
- * @param {string} id 组件 id
- * @param {object} params 组件参数
- */
- showSetting(setting, type, id, params = {}) {
- this.curComponentId = id;
- this.$emit('showSetting', setting, type, id, params);
- },
- /**
- * 组件中添加答案或解析
- * @param {'answer'|'analysis'} type 类型
- */
- addAnswerAndAnalysis(type) {
- this.findChildComponentByKey(`grid-${this.curComponentId}`)?.addAnswerAndAnalysis(type);
- },
- /**
- * 计算组件移动
- * @param {number} i 行
- * @param {number} j 列
- * @param {number} k 格子
- */
- componentMove(i, j, k) {
- return ({ type, offsetX, offsetY, id }) => {
- this.computedMoveData({ i, j, k, type, offsetX, offsetY, id });
- };
- },
- /**
- * 计算移动数据
- * @param {number} i 行
- * @param {number} j 列
- * @param {number} k 格子
- * @param {string} type 移动类型
- * @param {number} offsetX x 轴偏移量
- * @param {number} offsetY y 轴偏移量
- * @param {string} id 组件 id
- */
- computedMoveData({ i, j, k, type, offsetX, offsetY, id, min_width, min_height, row_width }) {
- const row = this.data.row_list[i];
- const col = row.col_list[j];
- const grid = col.grid_list[k];
- // 上下移动
- if (['top', 'bottom'].includes(type)) {
- this.handleVerticalMove(col, grid, offsetY, id, min_height);
- return;
- }
- // 一行中有多个格子
- let gridList = col.grid_list.filter((item) => item.row === grid.row);
- if (gridList.length > 1) {
- let find = gridList.findIndex((item) => item.id === id); // 当前 id 所在格子索引
- // 移动类型为 left 且不是第一个格子
- if (type === 'left' && find > 0) {
- this.handleMultGridLeftMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width });
- }
- // 移动类型为 right 且不是最后一个格子
- if (type === 'right' && find < gridList.length - 1) {
- this.handleMultGridRightMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width });
- }
- return;
- }
- // 移动类型为 left 且不是第一个格子
- if (type === 'left' && j > 0) {
- this.handleLeftMoveNotFirstGrid(row, j, offsetX, min_width, row_width);
- }
- // 移动类型为 right 且不是最后一个格子
- if (type === 'right' && j < row.col_list.length - 1) {
- this.handleRightMoveNotLastGrid(row, j, offsetX, min_width, row_width);
- }
- this.$forceUpdate();
- },
- /**
- * 处理垂直移动
- * @param {number} col 列数据
- * @param {object} grid 格子数据
- * @param {number} offsetY y 轴偏移量
- * @param {string} id 组件 id
- * @param {number} min_height 最小高度
- */
- handleVerticalMove(col, grid, offsetY, id, min_height = 0) {
- let height = 0;
- const _h = this.isEdit ? grid?.edit_height : grid.height;
- // 高度为 auto 时
- if (_h === 'auto' || _h === undefined) {
- const gridHeight = document.querySelector(`.${id}`).offsetHeight;
- height = gridHeight + offsetY;
- } else {
- // 高度为数字时
- const h = this.isEdit ? grid?.edit_height : grid.height;
- const gridHeight = Number(h?.replace('px', ''));
- height = gridHeight + offsetY;
- }
- // 当高度小于最小高度时,设置为最小高度
- height = Math.max(height, min_height, 50);
- if (this.isEdit) {
- grid.edit_height = `${height}px`;
- } else {
- grid.height = `${height}px`;
- }
- },
- /**
- * 处理左移且不是第一个格子
- * @param {object} row 行数据
- * @param {number} j 第几列
- * @param {number} offsetX x 轴偏移量
- * @param {number} min_width 最小宽度
- * @param {number} row_width 行宽度
- */
- handleLeftMoveNotFirstGrid(row, j, offsetX, min_width, row_width) {
- const prevGrid = row.width_list[j - 1];
- const prevWidth = Number(prevGrid.replace('fr', ''));
- const width = Number(row.width_list[j].replace('fr', ''));
- const max = prevWidth + width - 10;
- if (prevWidth + offsetX < 10 || prevWidth + offsetX > max || width - offsetX > max || width - offsetX < 10) {
- return;
- }
- // 计算拖动的距离与总宽度的比例
- const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
- const _w = width - ratio;
- if ((_w / 100) * row_width < min_width) {
- return;
- }
- row.width_list[j - 1] = `${prevWidth + ratio}fr`;
- row.width_list[j] = `${width - ratio}fr`;
- },
- /**
- * 处理一行中有多个格子的左移且不是第一个格子
- * @param {object} gridList 格子列表
- * @param {object} grid 格子数据
- * @param {object} col 列数据
- * @param {number} find 当前 id 所在格子索引
- * @param {number} k 格子的索引
- * @param {number} offsetX x 轴偏移量
- * @param {number} min_width 最小宽度
- */
- handleMultGridLeftMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width }) {
- const prevGrid = gridList[find - 1];
- const prevWidth = Number(prevGrid.width.replace('fr', ''));
- const width = Number(grid.width.replace('fr', ''));
- const max = prevWidth + width - 10;
- if (prevWidth + offsetX < 10 || prevWidth + offsetX > max || width - offsetX > max || width - offsetX < 10) {
- return;
- }
- // 计算拖动的距离与总宽度的比例
- const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
- const _w = width - ratio;
- if ((_w / 100) * row_width < min_width) {
- return;
- }
- col.grid_list[k - 1].width = `${prevWidth + ratio}fr`;
- grid.width = `${_w}fr`;
- },
- /**
- * 处理右移且不是最后一个格子
- * @param {object} row 行数据
- * @param {number} j 第几列
- * @param {number} offsetX x 轴偏移量
- * @param {number} min_width 最小宽度
- * @param {number} row_width 行宽度
- */
- handleRightMoveNotLastGrid(row, j, offsetX, min_width, row_width) {
- let nextGrid = row.width_list[j + 1];
- const nextWidth = Number(nextGrid.replace('fr', ''));
- const width = Number(row.width_list[j].replace('fr', ''));
- const max = nextWidth + width - 10;
- if (nextWidth - offsetX < 10 || nextWidth - offsetX > max || width + offsetX > max || width + offsetX < 10) {
- return;
- }
- const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
- const _w = width + ratio;
- if ((_w / 100) * row_width < min_width) {
- return;
- }
- row.width_list[j + 1] = `${nextWidth - ratio}fr`;
- row.width_list[j] = `${width + ratio}fr`;
- },
- /**
- * 处理一行中有多个格子的右移且不是最后一个格子
- * @param {object} gridList 格子列表
- * @param {object} grid 格子数据
- * @param {object} col 列数据
- * @param {number} find 当前 id 所在格子索引
- * @param {number} k 格子的索引
- * @param {number} offsetX x 轴偏移量
- * @param {number} min_width 最小宽度
- */
- handleMultGridRightMoveNotFirstGrid({ gridList, grid, col, find, k, offsetX, min_width, row_width }) {
- let nextGrid = gridList[find + 1];
- const nextWidth = Number(nextGrid.width.replace('fr', ''));
- const width = Number(grid.width.replace('fr', ''));
- const max = nextWidth + width - 10;
- if (nextWidth - offsetX < 10 || nextWidth - offsetX > max || width + offsetX > max || width + offsetX < 10) {
- return;
- }
- const ratio = (offsetX / document.querySelector('.row').offsetWidth) * 100;
- const _w = width + ratio;
- if ((_w / 100) * row_width < min_width) {
- return;
- }
- col.grid_list[k + 1].width = `${nextWidth - ratio}fr`;
- grid.width = `${width + ratio}fr`;
- },
- /**
- * 重新计算格子宽度
- * @param {number} i 行
- * @param {number} j 列
- */
- recalculateGridWidth(i, j) {
- let col = this.data.row_list[i].col_list[j];
- let grid_template_columns = '0';
- col.grid_list.forEach(({ width }, i) => {
- const w = `${width}`;
- if (i === col.grid_list.length - 1) {
- grid_template_columns += ` ${w} 0`;
- } else {
- grid_template_columns += ` ${w} `;
- }
- });
- col.grid_template_columns = grid_template_columns;
- },
- /**
- * 删除组件
- * @param {String} id 组件 id
- */
- deleteComponent(id) {
- const attrs = this.findChildComponentByKey(`grid-${id}`)?.$attrs;
- if (!attrs) return;
- const i = Number(attrs['data-row']);
- const j = Number(attrs['data-col']);
- const k = Number(attrs['data-grid']);
- const gridList = this.data.row_list[i].col_list[j].grid_list;
- let delRow = gridList[k].row; // 删除的 grid 的 row
- let delW = gridList[k].width; // 删除的 grid 的 width
- gridList.splice(k, 1);
- // 如果删除后没有 grid 了则删除列
- const colList = this.data.row_list[i].col_list[j];
- if (colList.grid_list.length === 0) {
- this.data.row_list[i].col_list.splice(j, 1);
- let width_list = this.data.row_list[i].width_list;
- const delW = width_list[j];
- width_list.splice(j, 1);
- this.data.row_list[i].width_list = width_list.map((item) => {
- return `${Number(item.replace('fr', '')) + Number(delW.replace('fr', '') / width_list.length)}fr`;
- });
- }
- // 如果删除后没有列了则删除行
- if (this.data.row_list[i].col_list.length === 0) {
- this.data.row_list.splice(i, 1);
- }
- // 如果删除后还有 grid 则重新计算 grid 的 row 和 width
- if (gridList?.length > 0) {
- let delNum = gridList.filter(({ row }) => row === delRow).length;
- let diff = Number(delW.replace('fr', '')) / delNum;
- if (delNum === 0) {
- // 删除 grid 后面的 row 都减 1
- gridList.forEach((item) => {
- if (item.row > delRow) {
- item.row -= 1;
- }
- });
- } else {
- gridList.forEach((item) => {
- if (item.row === delRow) {
- item.width = `${Number(item.width.replace('fr', '')) + diff}fr`;
- }
- });
- }
- }
- },
- computedColStyle(col) {
- let grid = col.grid_list;
- let maxCol = 0; // 最大列数
- let rowList = new Map();
- grid.forEach(({ row }) => {
- rowList.set(row, (rowList.get(row) || 0) + 1);
- });
- let curMaxRow = 0; // 当前列数最多的 row 的值
- rowList.forEach((value, key) => {
- if (value > maxCol) {
- maxCol = value;
- curMaxRow = key;
- }
- });
- // 计算 grid_template_areas
- let gridStr = '';
- let gridArr = [];
- let gridRowArr = []; // 存储不同 row 中间的 line
- grid.forEach(({ grid_area, row }, i) => {
- // 如果 gridArr[row - 1] 不存在则创建
- if (!gridArr[row - 1]) {
- gridArr[row - 1] = [];
- }
- if (row > 1 && grid[i - 1].row !== row) {
- gridRowArr.push({
- row: row - 1,
- str: `middle-${grid_area} `.repeat(maxCol * 3),
- });
- }
- // 如果当前 row 是最大列数的 row
- if (curMaxRow === row) {
- gridArr[row - 1].push(`left-${grid_area} ${grid_area} right-${grid_area}`);
- } else {
- let filter = grid.filter((item) => item.row === row);
- let find = filter.findIndex((item) => item.grid_area === grid_area);
- let needNum = (maxCol - filter.length) * 3; // 需要的数量
- let str = '';
- if (filter.length === 1) {
- str = ` ${grid_area} `.repeat(needNum + 1);
- } else {
- let arr = this.splitInteger(needNum, filter.length);
- str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
- }
- gridArr[row - 1].push(`left-${grid_area} ${str} right-${grid_area}`);
- }
- });
- gridArr.forEach((item, i) => {
- let find = gridRowArr.find((row) => row.row === i);
- if (find) {
- gridStr += `'${find.str}' `;
- }
- gridStr += `'${item.join(' ')}' `;
- });
- // 计算 grid_template_columns
- let gridTemCols = '';
- let max = { row: 0, num: 0 };
- grid.forEach(({ row }) => {
- // 计算出 row 的哪个值最多
- let len = grid.filter((item) => item.row === row).length;
- if (max.num < len) {
- max.num = len;
- max.row = row;
- }
- });
- grid.forEach((item) => {
- if (item.row === max.row) {
- gridTemCols += `${item.width} 4px 4px `;
- }
- });
- // 计算 grid_template_rows
- let gridTemplateRows = '';
- // 将 grid 按照 row 分组
- let gridMap = new Map();
- grid.forEach((item) => {
- if (!gridMap.has(item.row)) {
- gridMap.set(item.row, []);
- }
- gridMap.get(item.row).push(item?.edit_height || item.height);
- });
- gridMap.forEach((value, i) => {
- if (i > 1) {
- gridTemplateRows += '4px ';
- }
- if (value.length === 1) {
- gridTemplateRows += `${value[0]} `;
- } else {
- let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
- gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
- }
- });
- return {
- width: col.width,
- gridTemplateAreas: `'${'grid-top '.repeat(maxCol * 3)}' ${gridStr} '${'grid-bottom '.repeat(maxCol * 3)}'`,
- gridTemplateColumns: `0 ${gridTemCols.slice(0, gridTemCols.length - 8)} 0`,
- gridTemplateRows: `0 ${gridTemplateRows} 0`,
- };
- },
- /**
- * 分割整数为多个 3 的倍数
- * @param {number} num
- * @param {number} parts
- */
- splitInteger(num, parts) {
- let base = Math.floor(num / parts / 3) * 3;
- let arr = Array(parts).fill(base);
- let remainder = num - base * parts;
- for (let i = 0; remainder > 0; i = (i + 1) % parts) {
- arr[i] += 3;
- remainder -= 3;
- }
- return arr;
- },
- /**
- * 拖拽开始
- * 用点击模拟拖拽
- * @param {MouseEvent} event 鼠标事件
- * @param {string} type 组件类型
- * @param {object} params 组件参数
- */
- dragStart(event, type, params = {}) {
- // 获取鼠标位置
- const { clientX, clientY } = event;
- document.body.style.userSelect = 'none'; // 禁止选中文本
- this.drag.dragging = true;
- this.curType = type;
- this.curParams = params;
- // 在鼠标位置创建一个拖拽元素
- const dragging = document.createElement('div');
- dragging.className = `canvas-dragging${type === 'component' ? ' component-dragging' : ''}`;
- this.drag.clientX = clientX;
- this.drag.clientY = clientY;
- document.body.appendChild(dragging);
- },
- /**
- * 鼠标移动
- */
- dragMove(event) {
- if (!this.drag.dragging) return;
- const { clientX, clientY } = event;
- this.drag.clientX = clientX;
- this.drag.clientY = clientY;
- if (!this.isEdit) return; // 非编辑状态不允许显隐线
- let { isInsideCanvas } = this.getMarginDifferences();
- this.enterCanvas = isInsideCanvas;
- if (!isInsideCanvas) return;
- // 当移动组件时,如果鼠标位置距离画布上下边距小于 20px,则自动滚动画布,做一个定时器每隔 100ms 判断一次,直到鼠标位置距离边距大于 20px 或者离开画布
- if (this.curType === 'component') {
- const middleDom = document.querySelector('.create-middle');
- // 获取 middleDom 相对于 event 的位置
- const middleRect = middleDom.getBoundingClientRect();
- const middleHeight = middleRect.height;
- const offsetTop = clientY - middleRect.top;
- const offsetBottom = middleRect.bottom - clientY;
- if (this.scrollInterval) {
- clearInterval(this.scrollInterval);
- }
- if (offsetTop < 20) {
- this.scrollInterval = setInterval(() => {
- middleDom.scrollTop = Math.max(0, middleDom.scrollTop - 10);
- }, 10);
- } else if (offsetBottom < 20) {
- this.scrollInterval = setInterval(() => {
- middleDom.scrollTop = Math.min(middleDom.scrollHeight - middleHeight, middleDom.scrollTop + 10);
- }, 10);
- } else {
- clearInterval(this.scrollInterval);
- this.scrollInterval = null;
- }
- }
- this.showRecentLine(clientX, clientY);
- },
- /**
- * 显示最近的线
- * @param {number} clientX
- * @param {number} clientY
- */
- showRecentLine(clientX, clientY) {
- const dragLineList = document.querySelectorAll('.drag-line'); // 获取所有的横线
- const dragVerticalLineList = document.querySelectorAll('.drag-vertical-line'); // 获取所有的竖线
- let minDistance = Infinity;
- let minIndex = -1;
- const list = [...dragLineList, ...dragVerticalLineList];
- list.forEach((item, index) => {
- const rect = item.getBoundingClientRect();
- const distance = Math.sqrt(
- Math.pow(clientX - rect.left - rect.width / 2, 2) + Math.pow(clientY - rect.top - rect.height / 2, 2),
- );
- if (distance < minDistance) {
- minDistance = distance;
- minIndex = index;
- }
- });
- list.forEach((item, index) => {
- if (index === minIndex) {
- this.curRow = Number(item.getAttribute('data-row'));
- this.curCol = Number(item.getAttribute('data-col') || -1);
- this.curGrid = Number(item.getAttribute('data-grid') || -1);
- this.gridInsertType = item.getAttribute('data-type') || '';
- item.style.opacity = 1;
- } else {
- item.style.opacity = 0;
- }
- });
- },
- /**
- * 鼠标松开
- */
- dragEnd() {
- document.body.style.userSelect = 'auto';
- const dragging = document.querySelector('.canvas-dragging');
- if (dragging) {
- document.body.removeChild(dragging);
- this.drag.dragging = false;
- }
- if (this.scrollInterval) {
- clearInterval(this.scrollInterval);
- this.scrollInterval = null;
- }
- if (!this.isEdit) return;
- if (this.enterCanvas) {
- if (this.curType === 'component') {
- this.handleComponentMove();
- return;
- }
- if (this.curRow >= -1 && this.curCol <= -1) {
- this.calculateRowInsertedObject();
- }
- if (this.curRow >= -1 && this.curCol > -1 && this.curGrid <= -1) {
- this.calculateColObject();
- }
- if (this.curRow >= -1 && this.curCol > -1 && this.curGrid > -1) {
- this.calculateGridObject();
- }
- }
- this.curType = '';
- this.curParams = {};
- this.enterCanvas = false;
- },
- /**
- * 处理组件移动
- * 组件拖拽移动后,先删除组件,再根据拖拽位置计算插入行、列、格子
- */
- async handleComponentMove() {
- const component = this.findChildComponentByKey(`grid-${this.curParams.id}`);
- // 获取组件内容,保存后再插入到新的位置
- const requestData = component.getComponentRequestContent();
- const data = component?.data;
- this.curType = data?.type || 'select';
- let col = component?.$attrs['data-col'];
- let row = component?.$attrs['data-row'];
- let grid = component?.$attrs['data-grid'];
- let rowNum = 0; // 当前行组件数量
- this.data.row_list[row].col_list.forEach((item) => {
- rowNum += item.grid_list.length;
- });
- this.curParams = {
- ...this.curParams,
- col,
- row,
- grid,
- rowNum,
- };
- this.deleteComponent(this.curParams.id);
- await ContentSaveCoursewareComponentContent(requestData);
- if (this.curRow >= -1 && this.curCol <= -1) {
- this.calculateRowInsertedObject();
- }
- if (this.curRow >= -1 && this.curCol > -1 && this.curGrid <= -1) {
- // 当拖拽组件和放置位置在同一列内,且行内组件数量为 1,将 curRow 减 1,才能正确插入
- if ('col' in this.curParams) {
- const { row, rowNum } = this.curParams;
- if (row === this.curRow && rowNum === 1) {
- this.curRow = Math.max(0, this.curRow - 1);
- this.calculateRowInsertedObject();
- return;
- }
- }
- this.calculateColObject();
- }
- if (this.curRow >= -1 && this.curCol > -1 && this.curGrid > -1) {
- if ('col' in this.curParams) {
- const { col, row, grid } = this.curParams;
- // 当拖拽组件和放置位置在同一列内,将 curGrid 减 1,对应上面的删除组件,才能正确插入
- if (col === this.curCol && row === this.curRow && grid < this.curGrid) {
- this.curGrid = Math.max(0, this.curGrid - 1);
- }
- // 当拖拽组件和放置位置在同一列内,且行内组件数量为 1,将 curRow 减 1,才能正确插入
- if (col === this.curCol && row === this.curRow && grid === this.curGrid) {
- this.curRow = Math.max(0, this.curRow - 1);
- this.calculateRowInsertedObject();
- return;
- }
- }
- this.calculateGridObject();
- }
- this.curType = '';
- this.curParams = {};
- this.enterCanvas = false;
- },
- /**
- * 计算行样式
- * @param {number} i 行索引
- */
- computedRowStyle(i) {
- let row = this.data.row_list[i];
- let col = row.col_list;
- if (col.length <= 1) {
- return {
- gridTemplateColumns: '0 100fr 0',
- };
- }
- let str = row.width_list
- .map((item) => {
- return `${item}`;
- })
- .join(' 6px ');
- let gridTemplateColumns = `0 ${str} 0`;
- return {
- gridAutoFlow: 'column',
- gridTemplateColumns,
- gridTemplateRows: 'auto',
- };
- },
- /**
- * 计算网格插入的对象
- */
- calculateGridObject() {
- const id = this.curParams?.id || `ID-${getRandomNumber(12, true)}`;
- const letter = `L${getRandomNumber(6, true)}`;
- let row = this.data.row_list[this.curRow];
- let col = row.col_list[this.curCol];
- let grid = col.grid_list;
- let type = this.gridInsertType;
- if (['row', 'col-middle'].includes(type)) {
- let rowNum = this.curGrid === 0 ? 1 : grid[this.curGrid - 1].row + 1;
- grid.splice(this.curGrid, 0, {
- id,
- grid_area: letter,
- width: '100fr',
- height: 'auto',
- edit_height: 'auto',
- row: rowNum,
- type: this.curType,
- });
- // 在新加入的 grid 后面的 row 都加 1
- grid.forEach((item, i) => {
- if (i > this.curGrid) {
- item.row += 1;
- }
- });
- }
- if (['col-left', 'col-right'].includes(type)) {
- let rowNum = grid[type === 'col-left' ? this.curGrid : this.curGrid - 1].row;
- grid.splice(this.curGrid, 0, {
- id,
- grid_area: letter,
- width: '100fr',
- height: 'auto',
- edit_height: 'auto',
- row: rowNum,
- type: this.curType,
- });
- let allRowNum = grid.filter(({ row }) => row === rowNum).length;
- let w = 0;
- grid.forEach((item, i) => {
- if (item.row === rowNum && i !== this.curGrid) {
- let width = Number(item.width.replace('fr', ''));
- let diff = width / allRowNum;
- item.width = `${width - diff}fr`;
- w += diff;
- }
- });
- grid[this.curGrid].width = `${w}fr`;
- }
- },
- /**
- * 计算列插入的对象
- */
- calculateColObject() {
- const id = this.curParams?.id || `ID-${getRandomNumber(12, true)}`;
- const letter = `L${getRandomNumber(6, true)}`;
- const col_id = `C${getRandomNumber(8, true)}`;
- let row = this.data.row_list[this.curRow];
- let col = row.col_list;
- let w = 0;
- row.width_list.forEach((item, i) => {
- let itemW = Number(item.replace('fr', ''));
- let rowW = itemW / (row.width_list.length + 1);
- w += rowW;
- row.width_list[i] = `${itemW - rowW}fr`;
- });
- row.width_list.splice(this.curCol, 0, `${w}fr`);
- col.splice(this.curCol, 0, {
- col_id,
- width: '100fr',
- height: 'auto',
- grid_list: [
- {
- id,
- grid_area: letter,
- row: 1,
- width: '100fr',
- height: 'auto',
- edit_height: 'auto',
- type: this.curType,
- },
- ],
- });
- },
- /**
- * 计算行插入的对象
- */
- calculateRowInsertedObject() {
- const id = this.curParams?.id || `ID-${getRandomNumber(12, true)}`;
- const letter = `L${getRandomNumber(6, true)}`;
- const row_id = `R${getRandomNumber(6, true)}`;
- const col_id = `C${getRandomNumber(8, true)}`;
- this.data.row_list.splice(this.curRow + 1, 0, {
- width_list: ['100fr'],
- row_id,
- col_list: [
- {
- col_id,
- width: '100fr',
- height: 'auto',
- grid_list: [
- {
- id,
- grid_area: letter,
- width: '100fr',
- height: 'auto',
- edit_height: 'auto',
- row: 1,
- type: this.curType,
- },
- ],
- },
- ],
- });
- },
- /**
- * 获取拖拽元素和画布的边距差值
- * @returns {object} { leftMarginDifference, topMarginDifference, isInsideCanvas }
- * leftMarginDifference: 拖拽元素和画布左边距差值
- * topMarginDifference: 拖拽元素和画布上边距差值
- * isInsideCanvas: 是否在画布内
- */
- getMarginDifferences() {
- const rect1 = document.querySelector('.canvas-dragging').getBoundingClientRect();
- const rect2 = this.$refs.canvas.getBoundingClientRect();
- const leftMarginDifference = rect1.left - rect2.left + 128;
- const topMarginDifference = rect1.top - rect2.top + 72;
- let isInsideCanvas =
- leftMarginDifference > 0 &&
- leftMarginDifference < rect2.width &&
- topMarginDifference > 0 &&
- topMarginDifference < rect2.height;
- return { leftMarginDifference, topMarginDifference, isInsideCanvas };
- },
- // 获取子组件
- findChildComponentByKey(key) {
- return this.$refs.component.find((child) => child.$vnode.key === key);
- },
- /**
- * 计算分组线样式
- * @param {Array} rowIdList 行 ID 列表
- * @returns {Object} 样式对象
- */
- computedGroupLine(rowIdList) {
- // 获取画布顶部位置
- const canvas = this.$refs.canvas;
- const firstCheckbox = this.$el.querySelector(`.row-checkbox.${rowIdList[0]}`);
- const secCheckbox = this.$el.querySelector(`.row-checkbox.${rowIdList[1]}`);
- // DOM 未渲染,返回默认样式或空对象
- if (!canvas || !firstCheckbox || !secCheckbox) {
- return {};
- }
- const canvasTop = canvas.getBoundingClientRect().top;
- const firstRowTop = firstCheckbox.getBoundingClientRect().top;
- const secRowTop = secCheckbox.getBoundingClientRect().top;
- return {
- top: `${firstRowTop - canvasTop + 22}px`,
- height: `${secRowTop - firstRowTop - 24}px`,
- };
- },
- computedBorderColor(rowId) {
- const groupList = [];
- this.content_group_row_list.forEach(({ row_id, is_pre_same_group }) => {
- if (is_pre_same_group && groupList.length) {
- groupList[groupList.length - 1].push(row_id);
- } else {
- groupList.push([row_id]);
- }
- });
- const index = groupList.findIndex((g) => g.includes(rowId));
- return this.gridBorderColorList[index % this.gridBorderColorList.length];
- },
- /**
- * 计算网格样式
- * @param {Object} grid 网格对象
- * @returns {Object} 样式对象
- */
- computedGridStyle(grid) {
- let marginTop = grid.row === 1 ? '0' : '6px';
- return {
- gridArea: grid.grid_area,
- height: grid?.edit_height || grid.height,
- marginTop,
- };
- },
- /**
- * 计算网格视图顺序
- * @param {String} id 网格 ID
- * @returns {Number} 顺序值
- */
- computedGridViewOrder(id) {
- // 获取网格 id,是第几行第几列第几个网格,通过 data.row_list 计算
- let rowIndex = -1;
- let colIndex = -1;
- let gridIndex = -1;
- this.data.row_list.forEach((row, i) => {
- row.col_list.forEach((col, j) => {
- col.grid_list.forEach((grid, k) => {
- if (grid.id === id) {
- rowIndex = i;
- colIndex = j;
- gridIndex = k;
- }
- });
- });
- });
- let order = 0;
- // 计算前几行的网格数量
- if (rowIndex > 0) {
- for (let i = 0; i < rowIndex; i++) {
- this.data.row_list[i].col_list.forEach((col) => {
- order += col.grid_list.length;
- });
- }
- }
- // 计算当前行前几列的网格数量
- if (colIndex > 0) {
- for (let j = 0; j < colIndex; j++) {
- order += this.data.row_list[rowIndex].col_list[j].grid_list.length;
- }
- }
- // 加上当前列前面的网格数量
- order += gridIndex + 1;
- return order;
- },
- /**
- * 归一化 row_list 中的 row_id / col_id(为后端旧数据补 id)
- */
- normalizeRowColIds(data) {
- if (!data || !Array.isArray(data.row_list)) return data;
- data.row_list = data.row_list.map((row) => {
- if (!row.row_id) row.row_id = `R${getRandomNumber(6, true)}`;
- if (!Array.isArray(row.col_list)) row.col_list = [];
- row.col_list = row.col_list.map((col) => {
- if (!col.col_id) col.col_id = `C${getRandomNumber(8, true)}`;
- return col;
- });
- return row;
- });
- return data;
- },
- /**
- * 加载模板数据
- * @param {Object} param
- * @param {Object} param.data - 模板数据
- * @param {Array} param.content_group_row_list - 内容分组行列表
- */
- loadTemplateData_CreateCanvas({ data, content_group_row_list }) {
- this.data = data || {
- row_list: [],
- };
- this.content_group_row_list = content_group_row_list || [];
- },
- /**
- * 插入模板数据
- * @param {Object} param
- * @param {Object} param.row_list - 模板组件列表
- * @param {Array} param.content_group_row_list - 内容分组行列表
- */
- insertTemplateData_CreateCanvas({ row_list = [], content_group_row_list = [] }) {
- let contentGroupRowList = JSON.parse(JSON.stringify(content_group_row_list));
- let rowList = row_list.map((row) => {
- const oldRowID = row.row_id;
- const newRowID = `R${getRandomNumber(6, true)}`;
- row.row_id = newRowID;
- // 更新内容分组行列表中的 row_id
- for (let idx = 0; idx < contentGroupRowList.length; idx++) {
- if (contentGroupRowList[idx].row_id === oldRowID) {
- contentGroupRowList[idx] = { ...contentGroupRowList[idx], row_id: newRowID };
- }
- }
- row.col_list = row.col_list.map((col) => {
- col.col_id = `C${getRandomNumber(8, true)}`;
- col.grid_list = col.grid_list.map((grid) => {
- grid.oldId = grid.id;
- grid.id = `ID-${getRandomNumber(12, true)}`;
- grid.grid_area = `L${getRandomNumber(6, true)}`;
- return grid;
- });
- return col;
- });
- return row;
- });
- const curId = this.getCurSettingId();
- // 如果无选中组件,则插入到最后
- if (!curId) {
- this.data.row_list.push(...rowList);
- this.content_group_row_list.push(...contentGroupRowList);
- return;
- }
- // 插入到当前选中组件后面
- const attrs = this.findChildComponentByKey(`grid-${curId}`)?.$attrs;
- if (!attrs) return;
- const i = Number(attrs['data-row']);
- this.data.row_list.splice(i + 1, 0, ...rowList);
- this.content_group_row_list.splice(i + 1, 0, ...contentGroupRowList);
- },
- /**
- * @description 复制组件
- * @param {object} data 组件数据
- */
- copyComponent(data) {
- this.$emit('copyComponent', data);
- },
- /**
- * @description 粘贴组件
- * @param {Object} componentData 组件数据
- * @param {String} position 放入位置类型
- */
- pasteComponent(componentData, position) {
- if (!componentData) return;
- let _data = JSON.parse(JSON.stringify(componentData));
- const curId = this.getCurSettingId();
- const attrs = this.findChildComponentByKey(`grid-${curId}`)?.$attrs;
- if (!attrs) return;
- const i = Number(attrs['data-row']);
- const j = Number(attrs['data-col']);
- let k = Number(attrs['data-grid']);
- let type = _data.type;
- let row = this.data.row_list[i];
- let col = row.col_list[j];
- let grid = col.grid_list;
- const id = `ID-${getRandomNumber(12, true)}`;
- const letter = `L${getRandomNumber(6, true)}`;
- if (['top', 'bottom'].includes(position)) {
- let rowNum = k === 0 ? 1 : grid[k - 1].row + 1;
- if (position === 'bottom') {
- rowNum = grid[k].row + 1;
- }
- k = position === 'top' ? k : k + 1;
- grid.splice(k, 0, {
- id,
- grid_area: letter,
- width: '100fr',
- height: 'auto',
- edit_height: 'auto',
- row: rowNum,
- type,
- });
- grid.forEach((item, i) => {
- if (i > k) {
- item.row += 1;
- }
- });
- }
- if (['left', 'right'].includes(position)) {
- let rowNum = grid[k].row;
- let _k = position === 'left' ? k : k + 1;
- grid.splice(_k, 0, {
- id,
- grid_area: letter,
- width: '100fr',
- height: 'auto',
- edit_height: 'auto',
- row: rowNum,
- type,
- });
- let allRowNum = grid.filter(({ row }) => row === rowNum).length;
- let w = 0;
- grid.forEach((item, i) => {
- if (item.row === rowNum && i !== k) {
- let width = Number(item.width.replace('fr', ''));
- let diff = width / allRowNum;
- item.width = `${width - diff}fr`;
- w += diff;
- }
- });
- grid[_k].width = `${w}fr`;
- }
- this.$nextTick(() => {
- let newComponent = this.findChildComponentByKey(`grid-${id}`); // 获取新添加的组件实例
- newComponent?.setData(_data); // 设置新添加的组件数据
- });
- },
- // 交换组件位置
- switchComponent() {
- let checkedNum = 0;
- let components = [];
- this.$refs.component.forEach((child) => {
- if (child.$refs.base.checked) {
- checkedNum += 1;
- components.push(child.id);
- }
- });
- if (checkedNum !== 2) {
- this.$message.error('请选择两个组件进行交换');
- return;
- }
- this.$confirm('组件未保存的改动将丢失。确定要交换这两个组件的位置吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- })
- .then(() => {
- this.switchComponentData(components);
- })
- .catch(() => {
- // 取消交换
- });
- },
- /**
- * 合并遍历 data.row_list,找到两个网格对象并直接交换它们的属性
- * @param {Array} components 组件 ID 列表
- */
- switchComponentData(components) {
- const [id1, id2] = components;
- let g1 = null;
- let g2 = null;
- for (const row of this.data.row_list) {
- for (const col of row.col_list) {
- for (const grid of col.grid_list) {
- if (grid.id === id1) g1 = grid;
- else if (grid.id === id2) g2 = grid;
- if (g1 && g2) break;
- }
- if (g1 && g2) break;
- }
- if (g1 && g2) break;
- }
- if (!g1 || !g2) return;
- const tmp = { grid_area: g1.grid_area, id: g1.id, type: g1.type }; // 临时存储 g1 的属性
- g1.grid_area = g2.grid_area;
- g1.type = g2.type;
- g1.id = g2.id;
- g2.grid_area = tmp.grid_area;
- g2.type = tmp.type;
- g2.id = tmp.id;
- },
- clearContent() {
- this.data.row_list = [];
- this.content_group_row_list = [];
- },
- computedCanvasStyle() {
- const { background_image_url, background_position, background } = this.data;
- let canvasStyle = {
- '--bg-size': background_image_url ? `${background_position.width}% ${background_position.height}%` : '',
- '--bg-position': background_image_url ? `${background_position.left}% ${background_position.top}%` : '',
- '--bg-image': background_image_url ? `url(${background_image_url})` : '',
- '--bg-opacity': background_image_url && background.mode === 'image' ? background.image_opacity / 100 : 1,
- };
- if (background) {
- if (background.imageMode === 'fill') {
- canvasStyle['--bg-repeat'] = 'repeat';
- } else {
- canvasStyle['--bg-repeat'] = 'no-repeat';
- }
- if (background.imageMode === 'stretch') {
- canvasStyle['--bg-size'] = '100% 100%';
- }
- if (background.imageMode === 'adapt') {
- canvasStyle['--bg-size'] = 'contain';
- }
- if (background.mode === 'color') {
- canvasStyle['--bg-color'] = background.color;
- canvasStyle['--bg-image'] = '';
- canvasStyle['--bg-repeat'] = '';
- canvasStyle['--bg-position'] = '';
- canvasStyle['--bg-size'] = '';
- }
- if (background.enable_border) {
- canvasStyle['--bg-border'] =
- `${background.border_width}px ${background.border_style} ${background.border_color}`;
- } else {
- canvasStyle['--bg-border'] = 'none';
- }
- if (background.enable_radius) {
- canvasStyle['--bg-border-radius-top-left'] = `${background.top_left_radius}px`;
- canvasStyle['--bg-border-radius-top-right'] = `${background.top_right_radius}px`;
- canvasStyle['--bg-border-radius-bottom-left'] = `${background.bottom_left_radius}px`;
- canvasStyle['--bg-border-radius-bottom-right'] = `${background.bottom_right_radius}px`;
- }
- }
- return canvasStyle;
- },
- },
- };
- </script>
- <style lang="scss" scoped>
- .canvas {
- position: relative;
- z-index: 0;
- display: flex;
- flex-direction: column;
- row-gap: 8px;
- width: $courseware-width;
- min-height: calc(100% - 6px);
- padding: 24px;
- margin: 0 auto;
- background-color: #fff;
- background-repeat: no-repeat;
- &::before {
- position: absolute;
- top: 0;
- left: 0;
- z-index: -1; // 背景在最底层
- width: 100%;
- height: 100%;
- pointer-events: none; // 背景不接受鼠标事件
- content: '';
- background-color: #fff;
- background-image: var(--bg-image);
- background-repeat: var(--bg-repeat);
- background-position: var(--bg-position);
- background-size: var(--bg-size);
- border: var(--bg-border);
- border-radius: var(--bg-border-radius-top-left) var(--bg-border-radius-top-right)
- var(--bg-border-radius-bottom-right) var(--bg-border-radius-bottom-left);
- opacity: var(--bg-opacity);
- }
- .group-line {
- position: absolute;
- left: 11px;
- width: 1px;
- background-color: #165dff;
- }
- .row {
- display: grid;
- row-gap: 16px;
- .row-checkbox {
- position: absolute;
- left: 4px;
- }
- > .drag-vertical-line:not(:first-child, :last-child, .grid-line) {
- left: 6px;
- }
- .drag-vertical-line.grid-line-right,
- .drag-vertical-line.grid-line-left {
- top: 25%;
- height: 50%;
- }
- .drag-vertical-line.col-start {
- left: -12px;
- }
- .drag-vertical-line.col-end {
- right: -8px;
- }
- .col {
- display: grid;
- .drag-vertical-line:first-child {
- left: -8px;
- }
- .drag-vertical-line:not(:first-child, :last-child, .grid-line) {
- left: 6px;
- }
- .drag-vertical-line:last-child {
- right: -4px;
- }
- }
- }
- .drag-line {
- z-index: 2;
- width: calc(100% - 16px);
- height: 4px;
- margin: 0 8px;
- background-color: #379fff;
- border-radius: 4px;
- opacity: 0;
- &.drag-row {
- width: 50%;
- margin-left: 25%;
- background-color: $right-color;
- }
- &.grid-line:not(:first-child, :last-child) {
- position: relative;
- top: 6px;
- }
- &.grid-line {
- background-color: #f43;
- }
- }
- .drag-vertical-line {
- position: relative;
- z-index: 2;
- width: 4px;
- height: 100%;
- background-color: #f43;
- border-radius: 4px;
- opacity: 0;
- &.grid-line {
- background-color: #f43;
- }
- }
- }
- </style>
- <style lang="scss">
- .canvas-dragging {
- position: fixed;
- z-index: 999;
- width: 320px;
- height: 180px;
- background-color: #eaf5ff;
- border: 1px solid #b5dbff;
- border-radius: 4px;
- opacity: 0.5;
- transform: translate(-40%, -40%);
- &.component-dragging {
- background-color: #f9ffe0;
- border-color: #f9ffe0;
- }
- }
- </style>
|