| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808 |
- <template>
- <div ref="courserware" class="courserware" :style="computedCourserwareStyle()" @mouseup="handleTextSelection">
- <template v-for="(row, i) in data.row_list">
- <div v-show="computedRowVisibility(row.row_id)" :key="i" class="row" :style="getMultipleColStyle(i)">
- <el-checkbox
- v-if="
- isShowGroup &&
- groupRowList.find(({ row_id, is_pre_same_group }) => row.row_id === row_id && !is_pre_same_group)
- "
- v-model="rowCheckList[row.row_id]"
- :class="['row-checkbox', `${row.row_id}`]"
- />
- <!-- 列 -->
- <template v-for="(col, j) in row.col_list">
- <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
- <!-- 网格 -->
- <template v-for="(grid, k) in col.grid_list">
- <component
- :is="previewComponentList[grid.type]"
- :id="grid.id"
- :key="k"
- ref="preview"
- :content="computedColContent(grid.id)"
- :class="[grid.id]"
- :data-id="grid.id"
- :style="{
- gridArea: grid.grid_area,
- height: grid.height,
- }"
- @contextmenu.native.prevent="handleContextMenu($event, grid.id)"
- @handleHeightChange="handleHeightChange"
- />
- <div
- v-if="showMenu && componentId === grid.id"
- :key="'menu' + grid.id + k"
- class="custom-context-menu"
- :style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }"
- @click="handleMenuItemClick"
- >
- 添加批注
- </div>
- <div
- v-if="showRemark && Object.keys(componentRemarkObj).length !== 0 && componentRemarkObj[grid.id]"
- :key="'show' + grid.id + k"
- >
- <el-popover
- v-for="(items, indexs) in componentRemarkObj[grid.id]"
- :key="indexs"
- placement="bottom"
- width="200"
- trigger="click"
- >
- <div v-html="items.content"></div>
- <template #reference>
- <SvgIcon
- slot="reference"
- icon-class="icon-info"
- size="24"
- class="remark-info"
- :style="{ left: items.position_x - 12 + 'px', top: items.position_y - 12 + 'px' }"
- />
- </template>
- </el-popover>
- </div>
- </template>
- </div>
- </template>
- </div>
- </template>
- <!-- 选中文本的工具栏 -->
- <div v-show="showToolbar" class="contentmenu" :style="contentmenu">
- <span class="button" @click="setNote"><SvgIcon icon-class="sidebar-text" size="14" /> 笔记</span>
- <span class="line"></span>
- <span class="button" @click="setCollect"><SvgIcon icon-class="sidebar-collect" size="14" /> 收藏 </span>
- </div>
- </div>
- </template>
- <script>
- import { previewComponentList } from '@/views/book/courseware/data/bookType';
- export default {
- name: 'CoursewarePreview',
- provide() {
- return {
- getDragStatus: () => false,
- bookInfo: this.bookInfo,
- };
- },
- props: {
- data: {
- type: Object,
- default: () => ({}),
- },
- coursewareId: {
- type: String,
- default: '',
- },
- background: {
- type: Object,
- default: () => ({}),
- },
- componentList: {
- type: Array,
- required: true,
- },
- canRemark: {
- type: Boolean,
- default: false,
- },
- showRemark: {
- type: Boolean,
- default: false,
- },
- componentRemarkObj: {
- type: Object,
- default: () => ({}),
- },
- groupRowList: {
- type: Array,
- default: () => [],
- },
- isShowGroup: {
- type: Boolean,
- default: false,
- },
- groupShowAll: {
- type: Boolean,
- default: true,
- },
- project: {
- type: Object,
- default: () => ({}),
- },
- },
- data() {
- return {
- previewComponentList,
- courseware_id: this.coursewareId,
- bookInfo: {
- theme_color: '',
- },
- showMenu: false,
- divPosition: {
- left: 0,
- top: 0,
- }, // courserware盒子原始距离页面顶部和左边的距离
- menuPosition: { x: 0, y: 0, select_node: '' }, // 用于存储菜单的位置
- componentId: '', // 添加批注的组件id
- rowCheckList: {},
- showToolbar: false,
- contentmenu: {
- left: 0,
- top: 0,
- },
- selectedInfo: null,
- };
- },
- watch: {
- groupRowList: {
- handler(val) {
- if (!val) return;
- this.rowCheckList = val
- .filter(({ is_pre_same_group }) => !is_pre_same_group)
- .reduce((acc, row) => {
- acc[row.row_id] = false;
- return acc;
- }, {});
- },
- },
- coursewareId: {
- handler(val) {
- this.courseware_id = val;
- },
- },
- },
- mounted() {
- const element = this.$refs.courserware;
- const rect = element.getBoundingClientRect();
- this.divPosition = {
- left: rect.left,
- top: rect.top,
- };
- window.addEventListener('mousedown', this.handleMouseDown);
- },
- beforeDestroy() {
- window.removeEventListener('mousedown', this.handleMouseDown);
- },
- methods: {
- /**
- * 计算组件内容
- * @param {string} id 组件id
- * @returns {string} 组件内容
- */
- computedColContent(id) {
- if (!id) return '';
- return this.componentList.find((item) => item.component_id === id)?.content || '';
- },
- getMultipleColStyle(i) {
- let row = this.data.row_list[i];
- let col = row.col_list;
- if (col.length <= 1) {
- return {
- gridTemplateColumns: '100fr',
- };
- }
- let gridTemplateColumns = row.width_list.join(' ');
- return {
- gridAutoFlow: 'column',
- gridTemplateColumns,
- gridTemplateRows: 'auto',
- };
- },
- /**
- * 计算行的可见性
- * @params {string} rowId 行的ID
- */
- computedRowVisibility(rowId) {
- if (this.groupShowAll) return true;
- let { row_id, is_pre_same_group } = this.groupRowList.find(({ row_id }) => row_id === rowId);
- if (is_pre_same_group) {
- const index = this.groupRowList.findIndex(({ row_id }) => row_id === rowId);
- if (index === -1) return false;
- for (let i = index - 1; i >= 0; i--) {
- if (!this.groupRowList[i].is_pre_same_group) {
- return this.rowCheckList[this.groupRowList[i].row_id];
- }
- }
- return false;
- }
- return this.rowCheckList[row_id];
- },
- /**
- * 分割整数为多个 1的倍数
- * @param {number} num
- * @param {number} parts
- */
- splitInteger(num, parts) {
- let base = Math.floor(num / parts);
- let arr = Array(parts).fill(base);
- let remainder = num - base * parts;
- for (let i = 0; remainder > 0; i = (i + 1) % parts) {
- arr[i] += 1;
- remainder -= 1;
- }
- return arr;
- },
- computedColStyle(col) {
- const 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 gridTemplateAreas = '';
- let gridArr = [];
- grid.forEach(({ grid_area, row }) => {
- if (!gridArr[row - 1]) {
- gridArr[row - 1] = [];
- }
- if (curMaxRow === row) {
- gridArr[row - 1].push(`${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; // 需要的数量
- 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(`${str}`);
- }
- });
- gridArr.forEach((item) => {
- gridTemplateAreas += `'${item.join(' ')}' `;
- });
- // 计算 grid_template_columns
- let gridTemplateColumns = '';
- 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) {
- gridTemplateColumns += `${item.width} `;
- }
- });
- // 计算 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.height);
- });
- gridMap.forEach((value) => {
- 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,
- gridTemplateColumns,
- gridTemplateRows,
- };
- },
- /**
- * 计算课件背景样式
- * @returns {Object} 课件背景样式对象
- */
- computedCourserwareStyle() {
- const { background_image_url: bcImgUrl = '', background_position: pos = {} } = this.background || {};
- const hasNoRows = !Array.isArray(this.data?.row_list) || this.data.row_list.length === 0;
- const projectCover = this.project?.cover_image_file_url || '';
- // 优先在空行时使用背景图或项目封面
- const backgroundImage = hasNoRows ? bcImgUrl || projectCover : '';
- // 保护性读取位置/大小值,避免 undefined 导致字符串 "undefined%"
- const widthPct = typeof pos.width === 'undefined' ? '' : pos.width;
- const heightPct = typeof pos.height === 'undefined' ? '' : pos.height;
- const leftPct = typeof pos.left === 'undefined' ? '' : pos.left;
- const topPct = typeof pos.top === 'undefined' ? '' : pos.top;
- const hasBcImg = Boolean(bcImgUrl);
- const backgroundSize = hasBcImg ? `${widthPct}% ${heightPct}%` : hasNoRows ? 'contain' : '';
- const backgroundPosition = hasBcImg ? `${leftPct}% ${topPct}%` : hasNoRows ? 'center' : '';
- return {
- backgroundImage: backgroundImage ? `url(${backgroundImage})` : '',
- backgroundSize,
- backgroundPosition,
- };
- },
- handleContextMenu(event, id) {
- if (this.canRemark) {
- event.preventDefault(); // 阻止默认的上下文菜单显示
- this.menuPosition = {
- x: event.clientX - this.divPosition.left,
- y: event.clientY - this.divPosition.top,
- }; // 设置菜单位置
- this.componentId = id;
- this.$emit('computeScroll');
- }
- },
- handleResult(top, left, select_node) {
- this.menuPosition = {
- x: this.menuPosition.x + left,
- y: this.menuPosition.y + top,
- select_node,
- }; // 设置菜单位置
- this.showMenu = true; // 显示菜单
- },
- handleMenuItemClick() {
- this.showMenu = false; // 隐藏菜单
- this.$emit(
- 'addRemark',
- this.menuPosition.select_node,
- this.menuPosition.x,
- this.menuPosition.y,
- this.componentId,
- );
- },
- handleMouseDown(event) {
- if (event.button === 0 && event.target.className !== 'custom-context-menu') {
- // 0 表示左键
- this.showMenu = false;
- }
- },
- /**
- * 查找子组件
- * @param {string} id 组件的唯一标识符
- * @returns {Promise<HTMLElement|null>} 返回找到的子组件或 null
- */
- async findChildComponentByKey(id) {
- await this.$nextTick();
- return this.$refs.preview.find((child) => child.$el && child.$el.dataset && child.$el.dataset.id === id);
- },
- /**
- * 模拟回答
- * @param {boolean} isJudgingRightWrong 是否判断对错
- * @param {boolean} isShowRightAnswer 是否显示正确答案
- * @param {boolean} disabled 是否禁用
- */
- simulateAnswer(isJudgingRightWrong, isShowRightAnswer, disabled = true) {
- this.$refs.preview.forEach((item) => {
- item.showAnswer(isJudgingRightWrong, isShowRightAnswer, null, disabled);
- });
- },
- /**
- * 处理组件高度变化事件
- * @param {string} id 组件id
- * @param {string} newHeight 组件的新高度
- */
- handleHeightChange(id, newHeight) {
- this.data.row_list.forEach((row) => {
- row.col_list.forEach((col) => {
- col.grid_list.forEach((grid) => {
- if (grid.id === id) {
- grid.height = newHeight;
- }
- });
- });
- });
- },
- // 处理选中文本
- handleTextSelection() {
- this.showToolbar = false;
- // 延迟处理,确保选择已完成
- setTimeout(() => {
- const selection = window.getSelection();
- if (selection.toString().trim() === '') return null;
- const selectedText = selection.toString().trim();
- const range = selection.getRangeAt(0);
- console.info(selectedText, range);
- this.showToolbar = true;
- const container = document.querySelector('.courserware');
- const boxRect = container.getBoundingClientRect();
- const selectRect = range.getBoundingClientRect();
- this.contentmenu = {
- left: `${Math.round(selectRect.left - boxRect.left + selectRect.width / 2 - 63)}px`,
- top: `${Math.round(selectRect.top - boxRect.top + selectRect.height)}px`, // 向上偏移10px
- };
- this.selectedInfo = {
- text: selectedText,
- range,
- };
- }, 100);
- },
- // 笔记
- setNote() {
- this.showToolbar = false;
- this.oldRichData = {};
- let info = this.getSelectionInfo();
- if (!info) return;
- info.coursewareId = this.courseware_id;
- this.$emit('editNote', info);
- this.selectedInfo = null;
- },
- // 加入收藏
- setCollect() {
- this.showToolbar = false;
- let info = this.getSelectionInfo();
- if (!info) return;
- info.coursewareId = this.courseware_id;
- this.$emit('saveCollect', info);
- this.selectedInfo = null;
- },
- // 定位
- handLocation(item) {
- this.scrollToDataId(item.blockId);
- },
- getSelectionInfo() {
- if (!this.selectedInfo) return;
- const range = this.selectedInfo.range;
- let selectedText = this.selectedInfo.text;
- if (!selectedText) return null;
- let commonAncestor = range.commonAncestorContainer;
- if (commonAncestor.nodeType === Node.TEXT_NODE) {
- commonAncestor = commonAncestor.parentNode;
- }
- const blockElement = commonAncestor.closest('[data-id]');
- if (!blockElement) return null;
- const blockId = blockElement.dataset.id;
- // 获取所有汉字元素
- const charElements = blockElement.querySelectorAll('.py-char,.rich-text,.NNPE-chs');
- // 构建包含位置信息的文本数组
- const textFragments = Array.from(charElements)
- .map((el, index) => {
- let text = '';
- if (el.classList.contains('rich-text')) {
- const pElements = Array.from(el.querySelectorAll('p'));
- text = pElements.map((p) => p.textContent.trim()).join('');
- } else if (el.classList.contains('NNPE-chs')) {
- const spanElements = Array.from(el.querySelectorAll('span'));
- spanElements.push(el);
- text = spanElements.map((span) => span.textContent.trim()).join('');
- } else {
- text = el.textContent.trim();
- }
- // 过滤掉拼音和空文本
- if (!text || /^[a-zāáǎàōóǒòēéěèīíǐìūúǔùǖǘǚǜü]+$/i.test(text)) {
- return { text: '', element: el, index };
- }
- return { text, element: el, index };
- })
- .filter((fragment) => fragment.text);
- // 获取完整的纯文本
- const fullText = textFragments.map((f) => f.text).join('');
- // 清理选中文本
- let cleanSelectedText = selectedText.replace(/\n/g, '').trim();
- cleanSelectedText = cleanSelectedText.replace(/[a-zāáǎàōóǒòēéěèīíǐìūúǔùǖǘǚǜü]/gi, '').trim();
- if (!cleanSelectedText) return null;
- // 方案1A:使用Range的边界点精确定位
- try {
- const startContainer = range.startContainer;
- const endContainer = range.endContainer;
- const startOffset = range.startOffset;
- const endOffset = range.endOffset;
- // 找到选择开始的元素在textFragments中的位置
- let startFragmentIndex = -1;
- let cumulativeLength = 0;
- let startIndexInFullText = -1;
- for (let i = 0; i < textFragments.length; i++) {
- const fragment = textFragments[i];
- // 检查这个元素是否包含选择起点
- if (fragment.element.contains(startContainer) || fragment.element === startContainer) {
- // 计算在这个元素内的起始位置
- if (startContainer.nodeType === Node.TEXT_NODE) {
- // 如果是文本节点,需要计算在父元素中的偏移
- const elementText = fragment.text;
- startFragmentIndex = i;
- startIndexInFullText = cumulativeLength + Math.min(startOffset, elementText.length);
- break;
- } else {
- // 如果是元素节点,从0开始
- startFragmentIndex = i;
- startIndexInFullText = cumulativeLength;
- break;
- }
- }
- cumulativeLength += fragment.text.length;
- }
- if (startIndexInFullText === -1) {
- // 如果精确定位失败,回退到文本匹配(但使用更智能的匹配)
- return this.fallbackToTextMatch(fullText, cleanSelectedText, range, textFragments);
- }
- const endIndexInFullText = startIndexInFullText + cleanSelectedText.length;
- return {
- blockId,
- text: cleanSelectedText,
- startIndex: startIndexInFullText,
- endIndex: endIndexInFullText,
- fullText,
- };
- } catch (error) {
- console.warn('精确位置计算失败,使用备选方案:', error);
- return this.fallbackToTextMatch(fullText, cleanSelectedText, range, textFragments);
- }
- },
- // 备选方案:基于DOM位置的智能匹配
- fallbackToTextMatch(fullText, selectedText, range, textFragments) {
- // 获取选择范围的近似位置
- const rangeRect = range.getBoundingClientRect();
- // 找到最接近选择中心的文本片段
- let closestFragment = null;
- let minDistance = Infinity;
- textFragments.forEach((fragment) => {
- const rect = fragment.element.getBoundingClientRect();
- if (rect.width > 0 && rect.height > 0) {
- // 确保元素可见
- const centerX = rect.left + rect.width / 2;
- const centerY = rect.top + rect.height / 2;
- const rangeCenterX = rangeRect.left + rangeRect.width / 2;
- const rangeCenterY = rangeRect.top + rangeRect.height / 2;
- const distance = Math.sqrt(Math.pow(centerX - rangeCenterX, 2) + Math.pow(centerY - rangeCenterY, 2));
- if (distance < minDistance) {
- minDistance = distance;
- closestFragment = fragment;
- }
- }
- });
- if (closestFragment) {
- // 从最近的片段开始向前后搜索匹配
- const fragmentIndex = textFragments.indexOf(closestFragment);
- let cumulativeLength = 0;
- // 计算到当前片段的累计长度
- for (let i = 0; i < fragmentIndex; i++) {
- cumulativeLength += textFragments[i].text.length;
- }
- // 在当前片段附近搜索匹配
- const searchStart = Math.max(0, cumulativeLength - selectedText.length * 3);
- const searchEnd = Math.min(
- fullText.length,
- cumulativeLength + closestFragment.text.length + selectedText.length * 3,
- );
- const searchArea = fullText.substring(searchStart, searchEnd);
- const localIndex = searchArea.indexOf(selectedText);
- if (localIndex !== -1) {
- return {
- startIndex: searchStart + localIndex,
- endIndex: searchStart + localIndex + selectedText.length,
- text: selectedText,
- fullText,
- };
- }
- }
- // 最终回退:使用所有匹配位置,选择最合理的一个
- const allMatches = [];
- let searchIndex = 0;
- while ((searchIndex = fullText.indexOf(selectedText, searchIndex)) !== -1) {
- allMatches.push(searchIndex);
- searchIndex += selectedText.length;
- }
- if (allMatches.length === 1) {
- return {
- startIndex: allMatches[0],
- endIndex: allMatches[0] + selectedText.length,
- text: selectedText,
- fullText,
- };
- } else if (allMatches.length > 1) {
- // 如果有多个匹配,选择位置最接近选择中心的
- if (closestFragment) {
- let cumulativeLength = 0;
- let fragmentStartIndex = 0;
- for (let i = 0; i < textFragments.length; i++) {
- if (textFragments[i] === closestFragment) {
- fragmentStartIndex = cumulativeLength;
- break;
- }
- cumulativeLength += textFragments[i].text.length;
- }
- // 选择最接近当前片段起始位置的匹配
- const bestMatch = allMatches.reduce((best, current) => {
- return Math.abs(current - fragmentStartIndex) < Math.abs(best - fragmentStartIndex) ? current : best;
- });
- return {
- startIndex: bestMatch,
- endIndex: bestMatch + selectedText.length,
- text: selectedText,
- fullText,
- };
- }
- }
- return null;
- },
- scrollToDataId(dataId, offset) {
- if (!offset) offset = 0;
- const element = document.querySelector(`div[data-id="${dataId}"]`);
- if (element) {
- const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
- const offsetPosition = elementPosition - offset;
- element.scrollIntoView({
- behavior: 'smooth', // 滚动行为:'auto' | 'smooth'
- block: 'center', // 垂直对齐:'start' | 'center' | 'end' | 'nearest'
- inline: 'nearest', // 水平对齐:'start' | 'center' | 'end' | 'nearest'
- });
- }
- },
- },
- };
- </script>
- <style lang="scss" scoped>
- .courserware {
- position: relative;
- display: flex;
- flex-direction: column;
- row-gap: $component-spacing;
- width: 100%;
- height: 100%;
- min-height: calc(100vh - 226px);
- padding-top: $courseware-top-padding;
- padding-bottom: $courseware-bottom-padding;
- margin: 15px 0;
- background-color: #fff;
- background-repeat: no-repeat;
- border-bottom-right-radius: 12px;
- border-bottom-left-radius: 12px;
- &::before,
- &::after {
- position: absolute;
- left: 0;
- width: 100%;
- height: 15px;
- pointer-events: none;
- content: '';
- background: $courseware-bgColor;
- }
- &::before {
- top: -15px;
- }
- &::after {
- bottom: -15px;
- }
- .row {
- display: grid;
- gap: $component-spacing;
- .col {
- display: grid;
- gap: $component-spacing;
- overflow: hidden;
- }
- .row-checkbox {
- position: absolute;
- left: 4px;
- }
- }
- .custom-context-menu,
- .remark-info {
- position: absolute;
- z-index: 999;
- display: flex;
- gap: 3px;
- align-items: center;
- font-size: 14px;
- cursor: pointer;
- }
- .custom-context-menu {
- padding-left: 30px;
- background: url('../../../../assets/icon-publish.png') left center no-repeat;
- background-size: 24px;
- }
- .contentmenu {
- position: absolute;
- z-index: 999;
- display: flex;
- column-gap: 4px;
- align-items: center;
- padding: 8px;
- font-size: 14px;
- color: #000;
- background-color: #e7e7e7;
- border-radius: 4px;
- box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 30%);
- .svg-icon,
- .button {
- cursor: pointer;
- }
- .line {
- min-height: 16px;
- margin: 0 4px;
- }
- }
- }
- </style>
|