14 Commits

Author SHA1 Message Date
1f5867b568 feat: 🚀 简介 2025-11-25 14:30:19 +08:00
3aadf27102 feat: 🚀 角色权限-首页默认选择-父级半选中传给后台 2025-10-27 15:35:16 +08:00
5f423ba282 feat: 🚀 省略 2025-09-20 14:22:09 +08:00
f97dc8fd2e fix: 🧩 文章列表查询功能文章分类2级查询ID未传 2025-09-19 17:59:00 +08:00
f7979b4e9b feat: 🚀 生产环境更新 2025-09-18 11:58:42 +08:00
9110df9711 feat: 🚀 图片增加删除功能 2025-09-17 14:37:55 +08:00
1090351df7 feat: 🚀 图片排序 2025-09-17 13:54:30 +08:00
0dab4cc524 fix: 🧩 修复富文本编辑器视频上传限制(150M) 2025-08-26 17:27:49 +08:00
29d6ba59c9 feat: 🚀 切换站点清空语言 2025-08-07 14:51:54 +08:00
1566a72cb6 fix: 🧩 修復不能換行 2025-07-31 12:36:42 +08:00
4e8f3e6564 feat: 🚀 div标签解析 2025-07-30 16:33:56 +08:00
5da9c11771 feat: 🚀 生产环境 2025-07-29 12:01:11 +08:00
41da6b1914 feat: 🚀 tabs 2025-07-29 11:41:09 +08:00
1a97f75546 feat: 🚀 tabs增加滚动条 2025-07-28 10:03:07 +08:00
32 changed files with 3083 additions and 834 deletions

BIN
dist.zip

Binary file not shown.

View File

@@ -89,7 +89,6 @@ class RequestHttp {
// 可以在这里更新用户的 token 信息
const userStore = useUserStore();
userStore.setToken(authorization);
console.log("123232323");
return data;
}

View File

@@ -196,22 +196,23 @@
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="32px"]::before {
// content: "32px";
// }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="36px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before {
// content: "36px";
// }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="38px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="38px"]::before {
// content: "38px";
// }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="40px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="40px"]::before {
// content: "40px";
// }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="42px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="42px"]::before {
// content: "44px";
// }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="36px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before {
content: "36px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="38px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="38px"]::before {
content: "38px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="40px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="40px"]::before {
content: "40px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="42px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="42px"]::before {
content: "44px";
}
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="44px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="44px"]::before {
// content: "44px";

View File

@@ -54,7 +54,6 @@
@edit="handleTabsEdit"
@tab-change="handleTabChange"
>
<!-- 标签页标题支持编辑 -->
<el-tab-pane
:label="item.title"
:name="item.key"
@@ -65,17 +64,16 @@
>
<template #label>
<div class="tab-title-edit">
<!-- 文字显示状态 -->
<span v-if="!item.isEditing" @click="startEditTitle(index)" class="title-text">
{{ item.title }}
</span>
<!-- 输入框编辑状态 -->
<el-input
@click.stop
@keydown.delete.stop
@keydown.backspace.stop
v-else
v-model="item.title"
max-length=""
:ref="el => (editInputRefs[index] = el)"
size="small"
class="title-input"
@@ -84,7 +82,6 @@
</div>
</template>
<!-- 标签页编辑器内容 -->
<QuillEditor
:id="`tabEditor_${item.key}`"
:ref="
@@ -106,32 +103,93 @@
</template>
</el-dialog>
</div>
<!-- 图片顺序调整弹窗 -->
<el-dialog v-model="showImageSortDialog" title="调整图片顺序" width="800px" :before-close="handleImageDialogClose">
<div class="image-sort-container">
<draggable
v-model="sortedImageList"
:animation="200"
class="image-grid"
ghost-class="ghost"
item-key="customUid"
@update:modelValue="updateSortOrder"
>
<template #item="{ element: img }">
<div class="image-item">
<div class="image-preview-container">
<div class="image-order-badge">{{ img.sortOrder + 1 }}</div>
<img :src="img.tempUrl" :alt="`图片 ${img.sortOrder + 1}`" class="preview-img" />
<div style="display: flex; justify-content: flex-end; padding: 6px">
<el-button size="small" type="default" @click.stop="removeImage(img.customUid)">删除</el-button>
</div>
</div>
<div class="image-info">
<span class="image-name">{{ img.name }}</span>
</div>
</div>
</template>
</draggable>
<div v-if="sortedImageList.length === 0" class="empty-state">暂无图片请先上传</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showImageSortDialog = false">取消</el-button>
<el-button type="primary" @click="confirmInsertImages" :disabled="sortedImageList.length === 0">
确认插入
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup name="Editor">
// import { Delete } from "@element-plus/icons-vue";
import { QuillEditor, Quill } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { getCurrentInstance, reactive, ref, toRaw, computed, onMounted, nextTick } from "vue";
import { generateUUID } from "@/utils";
import { h } from "@/utils/url";
import { routerObj } from "./utils.js";
import { titleConfig } from "./titleConfig.js";
import { uploadVideo, uploadImg } from "@/api/modules/upload";
import { ElNotification } from "element-plus";
import { useRouter } from "vue-router";
import { useMsg } from "@/hooks/useMsg";
import draggable from "vuedraggable";
// 字体配置
let fontSizeStyle = Quill.import("attributors/style/size");
fontSizeStyle.whitelist = ["12px", "14px", "16px", "18px", "20px", "22px", "24px", "26px", "28px", "30px", "32px"];
fontSizeStyle.whitelist = [
"12px",
"14px",
"16px",
"18px",
"20px",
"22px",
"24px",
"26px",
"28px",
"30px",
"32px",
"34px",
"36px",
"38px",
"40px",
"42px"
];
Quill.register(fontSizeStyle, true);
// 自定义Blot
import ImageBlot from "./quill-image";
import Video from "./quill-video";
import TabsBlot from "./quill-tabs";
import DynamicDivBlot from "./quill-detail-div";
Quill.register(Video);
Quill.register(ImageBlot);
Quill.register(TabsBlot);
Quill.register(DynamicDivBlot);
// 基础变量
const { proxy } = getCurrentInstance();
@@ -144,36 +202,44 @@ const uploadFileVideo = ref(null);
const outerVisible = ref(false);
const imageList = ref([]);
const imageListDb = ref([]);
const activeName = ref(null); // 跟踪当前激活的标签页key
const activeEditor = ref("main"); // 跟踪当前活跃编辑器main/tab-索引
const activeName = ref(null);
const activeEditor = ref("main");
// 标签页数据新增key作为唯一标识isEditing控制编辑状态
// 标签页数据
const tabsData = ref([]);
// 标签页编辑器ref数组
const tabEditors = ref([]);
// 标题编辑输入框的ref
const editInputRefs = ref([]);
const currentEditingTabsRef = ref(null);
// 图片排序相关变量
const showImageSortDialog = ref(false);
const sortedImageList = ref([]);
const uploadingCount = ref(0);
// Props
const props = defineProps({
content: { type: String, default: "" },
readOnly: { type: Boolean, default: false },
fileSizeLimit: { type: Number, default: 10 }
fileSizeLimit: { type: Number, default: 5 }
});
// 主编辑器内容双向绑定
const editorContent = computed({
get: () => props.content,
get: () => {
if (!props.content) return "";
return props.content;
},
set: val => {
emit("update:content", val);
}
});
const myQuillEditor = ref(null); // 主编辑器ref
const myQuillEditor = ref(null);
// 主编辑器配置(保持不变)
// 主编辑器配置
const options = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -192,6 +258,7 @@ const options = reactive({
image: function (value) {
if (value) {
activeEditor.value = "main";
sortedImageList.value = [];
proxy.$refs.uploadRef.click();
} else Quill.format("customImage", true);
},
@@ -211,10 +278,11 @@ const options = reactive({
readOnly: props.readOnly
});
// 标签页编辑器配置(保持不变)
// 标签页编辑器配置
const options1 = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -234,6 +302,7 @@ const options1 = reactive({
if (value) {
const currentIndex = tabsData.value.findIndex(item => item.key === activeName.value);
activeEditor.value = `tab-${currentIndex}`;
sortedImageList.value = [];
proxy.$refs.uploadRef.click();
} else Quill.format("customImage", true);
},
@@ -251,33 +320,41 @@ const options1 = reactive({
readOnly: props.readOnly
});
// 上传前校验(保持不变)
// 上传前校验
const handleBeforeUpload = file => {
const fileType = file.type;
file.customUid = generateUUID();
imageListDb.value.push(file);
const validTypes = ["image/jpeg", "image/png", "image/gif", "image/jpg", "image/bmp", "image/webp"];
if (!validTypes.includes(fileType)) {
ElNotification({ title: "格式错误", message: "仅支持图片格式", type: "warning" });
imageListDb.value = imageListDb.value.filter(item => item.customUid !== file.customUid);
return false;
}
const isLt = file.size / 1024 / 1024 < props.fileSizeLimit;
if (!isLt) {
ElNotification({ title: "大小超限", message: `不能超过 ${props.fileSizeLimit} MB`, type: "warning" });
imageListDb.value = imageListDb.value.filter(item => item.customUid !== file.customUid);
return false;
}
// 生成临时URL用于预览
const tempUrl = URL.createObjectURL(file);
imageListDb.value.push({
...file,
tempUrl,
sortOrder: imageListDb.value.length, // 初始化排序索引
serverImgId: "",
path: ""
});
uploadingCount.value++;
return true;
};
// 图片上传(保持不变)
// 图片上传处理
const handleHttpUpload = async options => {
let formData = new FormData();
formData.append("image", options.file);
imageList.value.push(options.file);
try {
const result = await uploadImg(formData, routerName.value, options.file.customUid);
@@ -285,77 +362,200 @@ const handleHttpUpload = async options => {
const { data } = result.data;
const { imgId } = result;
const fileItem = imageListDb.value.find(item => item.customUid === imgId);
const fileItem = imageListDb.value.find(item => item.customUid === options.file.customUid);
if (fileItem) {
fileItem.serverImgId = imgId;
fileItem.path = data.path;
}
const allFilesUploaded = imageListDb.value.every(item => item.path);
if (allFilesUploaded) {
let rawQuillEditor = "";
let quill = "";
if (activeEditor.value === "main") {
rawQuillEditor = toRaw(myQuillEditor.value);
quill = rawQuillEditor.getQuill();
} else {
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
quill = rawQuillEditor.getQuill();
}
uploadingCount.value--;
imageListDb.value.forEach(item => {
const length = quill.getLength() - 1;
quill.insertEmbed(length, "customImage", {
url: h + item.path,
id: item.serverImgId || generateUUID()
});
quill.setSelection(length + 1);
// 所有图片上传完成后显示排序弹窗
if (uploadingCount.value === 0) {
sortedImageList.value = [...imageListDb.value];
updateSortOrder(); // 确保排序索引正确
nextTick(() => {
showImageSortDialog.value = true;
});
const finalLength = quill.getLength();
quill.setSelection(finalLength);
imageList.value = [];
imageListDb.value = [];
}
}
} catch (error) {
console.error("图片上传失败:", error);
imageList.value = imageList.value.filter(item => item.customUid !== options.file.customUid);
imageListDb.value = imageListDb.value.filter(item => item.customUid !== options.file.customUid);
const failUid = options.file.customUid;
imageListDb.value = imageListDb.value.filter(item => item.customUid !== failUid);
uploadingCount.value = Math.max(0, uploadingCount.value - 1);
ElNotification({
title: "上传失败",
message: `图片 ${options.file.name} 上传失败`,
type: "error"
});
}
};
// 视频上传(保持不变)
// 更新排序索引
const updateSortOrder = () => {
sortedImageList.value.forEach((item, index) => {
item.sortOrder = index;
});
};
// 从排序列表移除图片
const removeImage = customUid => {
// 释放临时URL
const removedImg = sortedImageList.value.find(img => img.customUid === customUid);
if (removedImg?.tempUrl) {
URL.revokeObjectURL(removedImg.tempUrl);
}
// 从列表中移除
sortedImageList.value = sortedImageList.value.filter(img => img.customUid !== customUid);
// 重新计算排序索引
updateSortOrder();
};
// 确认插入图片到编辑器
const confirmInsertImages = () => {
let quill;
if (activeEditor.value === "main") {
const rawQuillEditor = toRaw(myQuillEditor.value);
quill = rawQuillEditor.getQuill();
} else {
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
const rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
quill = rawQuillEditor.getQuill();
}
if (!quill) {
ElNotification({ title: "错误", message: "编辑器未加载完成", type: "error" });
return;
}
// 获取光标位置
const range = quill.getSelection() || { index: 0 };
let currentInsertIndex = range.index;
// 按排序索引升序插入
const sortedByOrder = [...sortedImageList.value].sort((a, b) => a.sortOrder - b.sortOrder);
//
sortedByOrder.forEach(item => {
quill.insertEmbed(currentInsertIndex, "customImage", {
url: item.path,
id: item.serverImgId || generateUUID()
});
currentInsertIndex++;
});
// 调整光标位置
quill.setSelection(currentInsertIndex);
// 清理资源
sortedImageList.value.forEach(img => {
if (img.tempUrl) {
URL.revokeObjectURL(img.tempUrl);
}
});
// 关闭弹窗并重置
showImageSortDialog.value = false;
imageList.value = [];
imageListDb.value = [];
sortedImageList.value = [];
};
// 关闭图片排序弹窗时清理资源
const handleImageDialogClose = () => {
// 释放所有临时URL
sortedImageList.value.forEach(img => {
if (img.tempUrl) {
URL.revokeObjectURL(img.tempUrl);
}
});
// 重置状态
showImageSortDialog.value = false;
imageList.value = [];
imageListDb.value = [];
sortedImageList.value = [];
};
// 视频上传
const handleVideoUpload = async evt => {
if (evt.target.files.length === 0) return;
const formData = new FormData();
formData.append("video", evt.target.files[0]);
try {
let rawQuillEditor = "";
let quill = "";
if (activeEditor.value === "main") {
rawQuillEditor = toRaw(myQuillEditor.value);
quill = rawQuillEditor.getQuill();
} else {
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
quill = rawQuillEditor.getQuill();
}
let length = quill.selection.savedRange.index;
const { data } = await uploadVideo(formData);
quill.insertEmbed(length, "customVideo", {
url: h + data.path,
id: generateUUID()
const file = evt.target.files[0];
// 校验视频文件
const maxSize = 150 * 1024 * 1024;
if (file.size > maxSize) {
ElNotification({
title: "文件过大",
message: `视频大小不能超过 ${150}MB`,
type: "warning"
});
uploadFileVideo.value.value = "";
evt.target.value = "";
return;
}
// 生成视频本地URL
const localVideoUrl = URL.createObjectURL(file);
try {
// 上传视频
const videoFormData = new FormData();
videoFormData.append("video", file);
const videoRes = await uploadVideo(videoFormData);
if (videoRes?.code !== 0) {
throw new Error(`视频上传失败: ${videoRes?.message || "未知错误"}`);
}
const videoUrl = videoRes.data.path;
// 生成封面图并上传
const frameBlob = await Video.captureVideoFrame(localVideoUrl);
let coverUrl = "";
if (frameBlob) {
const coverFormData = new FormData();
const coverUid = generateUUID();
coverFormData.append("image", frameBlob, `cover-${coverUid}.jpg`);
const coverRes = await uploadImg(coverFormData, routerName.value, coverUid);
if (coverRes?.data?.code === 0) {
coverUrl = coverRes.data.data.path;
}
}
// 插入视频到编辑器
insertVideoToEditor(videoUrl, coverUrl);
} catch (error) {
console.log(error);
} finally {
URL.revokeObjectURL(localVideoUrl);
evt.target.value = "";
}
};
// 标签页切换事件基于key切换
// 插入视频到编辑器
const insertVideoToEditor = (videoUrl, coverUrl) => {
let quill;
if (activeEditor.value === "main") {
quill = toRaw(myQuillEditor.value)?.getQuill();
} else {
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
quill = toRaw(tabEditors.value[tabIndex])?.getQuill();
}
if (quill) {
const range = quill.getSelection() || { index: 0 };
quill.insertEmbed(range.index, "customVideo", {
url: videoUrl,
poster: coverUrl
});
quill.setSelection(range.index + 1);
}
};
// 标签页切换事件
const handleTabChange = key => {
const tabIndex = tabsData.value.findIndex(item => item.key === key);
activeName.value = key;
@@ -368,37 +568,32 @@ const handleTabsEdit = (targetKey, action) => {
if (tabsData.value.length > 5) {
return useMsg("error", "标签页已达上限 !");
}
// 新增标签页生成唯一key默认标题初始不处于编辑状态
const newKey = `tab_${generateUUID()}`;
const newIndex = tabsData.value.length;
tabsData.value.push({
key: newKey,
title: `标签${newIndex + 1}`,
content: "",
isEditing: false // 新增时默认不编辑
isEditing: false
});
nextTick(() => {
activeName.value = newKey;
activeEditor.value = `tab-${newIndex}`;
// 新增后自动进入编辑状态
setTimeout(() => {
startEditTitle(newIndex);
}, 100);
});
} else if (action === "remove") {
// 删除标签页
const index = tabsData.value.findIndex(item => item.key === targetKey);
tabsData.value.splice(index, 1);
tabEditors.value.splice(index, 1);
editInputRefs.value.splice(index, 1);
// 调整活跃编辑器索引
if (activeEditor.value.startsWith("tab-")) {
const currentTabIndex = parseInt(activeEditor.value.split("-")[1]);
if (currentTabIndex > index) {
activeEditor.value = `tab-${currentTabIndex - 1}`;
} else if (currentTabIndex === index) {
// 若删除当前活跃标签,切换到第一个或主编辑器
activeEditor.value = tabsData.value.length > 0 ? "tab-0" : "main";
activeName.value = tabsData.value.length > 0 ? tabsData.value[0].key : null;
}
@@ -410,48 +605,41 @@ const handleTabsEdit = (targetKey, action) => {
const startEditTitle = index => {
const tab = tabsData.value[index];
if (!tab) return;
// 记录原始标题(用于取消编辑时恢复)
tab.originalTitle = tab.title;
tab.isEditing = true;
// 延迟获取焦点,确保输入框已渲染
nextTick(() => {
editInputRefs.value[index]?.focus();
});
};
// 完成编辑(失去焦点或回车)
// 完成编辑标签页标题
const finishEditTitle = index => {
const tab = tabsData.value[index];
if (!tab) return;
// 校验标题(不能为空)
if (!tab.title.trim()) {
tab.title = tab.originalTitle || `标签${index + 1}`;
ElNotification({ title: "提示", message: "标签标题不能为空", type: "info" });
}
tab.isEditing = false;
// 更新activeName如果当前编辑的是活跃标签
if (tab.key === activeName.value) {
activeName.value = tab.key; // 触发重绘
activeName.value = tab.key;
}
};
// 其他方法(保持不变)
// 其他方法
const onContentChange = content => {
emit("handleRichTextContentChange", content);
emit("update:content", content);
};
const setTabsInfo = () => {
outerVisible.value = false;
//清空
tabsData.value = [];
activeName.value = null;
activeEditor.value = "main";
};
//弹窗关闭前的钩子
const handleBeforeClose = () => {
setTabsInfo();
};
// 确认按钮点击事件(修改后)
const handleQR = () => {
const quill = toRaw(myQuillEditor.value)?.getQuill();
if (!quill) return;
@@ -459,23 +647,17 @@ const handleQR = () => {
return useMsg("error", "标签页内容为空 !");
}
const range = quill.getSelection(true);
// 判断是否是编辑已有标签页(通过 currentEditingTabsRef 是否有值)
if (currentEditingTabsRef.value) {
// 1. 编辑模式:更新原有标签页组件
const blot = currentEditingTabsRef.value;
// 更新 blot 的数据(触发 DOM 更新)
blot.updateContents(tabsData.value); // 需要在 TabsBlot 中添加 updateContents 方法
// 清除编辑状态标记
currentEditingTabsRef.value.updateContents(tabsData.value);
currentEditingTabsRef.value = null;
} else {
// 2. 新增模式:插入新的标签页组件
quill.insertEmbed(range.index, "tabs", tabsData.value);
quill.setSelection(range.index + 1);
quill.insertText(range.index, "\n");
}
// 关闭弹窗并清空临时数据
setTabsInfo();
};
//取消
const handleQX = () => {
setTabsInfo();
};
@@ -487,20 +669,16 @@ const initTitle = () => {
if (tip) tip.setAttribute("title", item.title);
});
};
// 定义 loadTabsDataToEditor 函数
const loadTabsDataToEditor = tabs => {
// 清空现有数据
tabsData.value = [];
// 转换原始标签数据为编辑所需格式添加key和编辑状态
tabs.forEach((tab, index) => {
tabsData.value.push({
key: `tab_${generateUUID()}`, // 生成唯一key
title: tab.title || `标签${index + 1}`, // 避免空标题
content: tab.content || "", // 标签页内容
isEditing: false // 编辑状态标记
key: `tab_${generateUUID()}`,
title: tab.title || `标签${index + 1}`,
content: tab.content || "",
isEditing: false
});
});
// 激活第一个标签页(如果有数据)
nextTick(() => {
if (tabsData.value.length > 0) {
activeName.value = tabsData.value[0].key;
@@ -511,21 +689,13 @@ const loadTabsDataToEditor = tabs => {
onMounted(() => {
initTitle();
// 监听编辑按钮点击事件
const editorEl = document.querySelector(".ql-editor");
if (editorEl) {
editorEl.addEventListener("edit-tabs", e => {
console.log(1232, "测试");
console.log(e.detail.blot, "=e.detail=");
const tabsData = TabsBlot.value(e.detail.blot.domNode);
console.log(tabsData, "=tabsData=");
if (tabsData.length > 0) {
// 保存当前编辑的标签页引用
currentEditingTabsRef.value = e.detail.blot;
console.log(currentEditingTabsRef.value, "=currentEditingTabsRef.value =");
// 加载数据到弹窗
loadTabsDataToEditor(tabsData);
// 显示弹窗
outerVisible.value = true;
}
});
@@ -546,27 +716,226 @@ defineExpose({
<style lang="scss">
@import "./index.scss";
// 增加编辑器内容区交互性确保删除可用
// 编辑器基础样式
.ql-editor {
min-height: 600px; // 确保空编辑器也有点击区域
min-height: 600px;
cursor: text !important;
user-select: text !important;
}
// /* 标签页样式 */
// 图片排序弹窗样式
.image-sort-container {
margin-top: 15px;
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
padding: 10px;
}
.image-item {
width: 150px;
padding: 8px;
cursor: move;
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
&:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
}
}
// 图片预览容器
.image-preview-container {
position: relative;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
}
.preview-img {
display: block;
width: 100%;
height: 120px;
object-fit: cover;
}
// 排序序号徽章
.image-order-badge {
position: absolute;
top: 5px;
left: 5px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: 12px;
color: white;
background: rgb(0 0 0 / 50%);
border-radius: 50%;
}
// 圆形删除按钮
// .circle-delete-btn {
// position: absolute;
// top: -8px;
// right: -8px;
// z-index: 500;
// display: flex;
// align-items: center;
// justify-content: center;
// width: 28px;
// height: 28px;
// padding: 0;
// margin: 0;
// color: white;
// background-color: #ff4d4f;
// border: none;
// border-radius: 50%;
// box-shadow: 0 2px 4px rgb(0 0 0 / 20%);
// transition: all 0.2s;
// &:hover {
// color: white;
// background-color: #d93025;
// transform: scale(1.1);
// }
// .el-icon {
// font-size: 14px;
// }
// }
.image-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
font-size: 12px;
}
.image-name {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-state {
padding: 30px 0;
font-size: 14px;
color: #666666;
text-align: center;
}
.ghost {
background-color: #e9ecef;
opacity: 0.5;
}
// 标签页样式
.quill-tabs {
margin: 15px 0;
overflow: hidden;
border-radius: 4px;
}
// /* 用伪元素添加图标(可替换为自己的图标) */
.ql-tabs::before {
font-size: 16px;
content: "T"; /* 用 emoji 或字体图标 */
content: "T";
}
.title-input {
width: 100px;
margin: -2px 0; /* 与标签对齐 */
margin: -2px 0;
}
// 图片样式
.ql-editor .quill-image {
max-width: 100%;
height: auto;
margin: 5px 0;
&:focus {
outline: 2px solid #4285f4;
outline-offset: 2px;
}
}
// 详情样式
.o_detail_all {
overflow: hidden;
text-align: center;
background-color: #ffffff;
}
.o_detail_title {
margin-top: 2vw;
margin-bottom: 1.25vw;
overflow: hidden;
font-size: 2.25em;
font-weight: 600;
line-height: 1.2em;
color: #101010;
text-align: center;
background-color: #ffffff;
}
.o_detail_small {
margin-bottom: 0.7vw;
font-size: 1.5em;
color: #333333;
}
.o_detail_text {
width: 80%;
margin-right: auto;
margin-bottom: 0.7vw;
margin-left: auto;
font-size: 1.125em;
line-height: 1.5em;
color: #737373;
}
.products_des {
width: 100%;
margin-bottom: 50px;
}
.products_des img {
width: 100%;
}
.de_t_n {
font-size: 1.5em;
color: #333333;
}
.detail_title {
padding: 2% 0;
text-align: center;
}
.detail_title p {
line-height: 2em;
}
.detail_con_a {
margin: auto;
overflow: hidden;
}
.lj_detail_text,
.lj_detail_texts {
font-size: 0.875em;
}
.lj_detail_text p {
padding: 0.5% 0;
line-height: 1.6em;
}
/* seo-pro */
.seo-pro h3 {
margin: 2% 0 1%;
font-size: 1.5em;
font-weight: 400;
line-height: 1.2;
color: #333333;
text-align: center;
}
.seo-pro p {
margin: 0 0 11px;
text-align: center;
}
.seo-pro a {
color: #333333;
text-decoration: none;
}
.sa_blue,
.sa_blue a,
.seo-pro a:hover {
color: #009fdf;
}
</style>

View File

@@ -55,7 +55,14 @@
@tab-change="handleTabChange"
>
<!-- 标签页标题支持编辑 -->
<el-tab-pane :label="item.title" :name="item.key" v-for="(item, index) in tabsData" :key="item.key">
<el-tab-pane
:label="item.title"
:name="item.key"
v-for="(item, index) in tabsData"
:key="item.key"
@keydown.delete.stop
@keydown.backspace.stop
>
<template #label>
<div class="tab-title-edit">
<!-- 文字显示状态 -->
@@ -64,14 +71,16 @@
</span>
<!-- 输入框编辑状态 -->
<el-input
@click.stop
@keydown.delete.stop
@keydown.backspace.stop
v-else
v-model="item.title"
max-length=""
:ref="el => (editInputRefs[index] = el)"
size="small"
class="title-input"
@blur="finishEditTitle(index)"
@keyup.enter="finishEditTitle(index)"
@keyup.esc="cancelEditTitle(index)"
/>
</div>
</template>
@@ -105,26 +114,44 @@ import { QuillEditor, Quill } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { getCurrentInstance, reactive, ref, toRaw, computed, onMounted, nextTick } from "vue";
import { generateUUID } from "@/utils";
import { h } from "@/utils/url";
// import { h } from "@/utils/url";
import { routerObj } from "./utils.js";
import { titleConfig } from "./titleConfig.js";
import { uploadVideo, uploadImg } from "@/api/modules/upload";
import { ElNotification } from "element-plus";
import { useRouter } from "vue-router";
import { useMsg } from "@/hooks/useMsg";
// 字体配置
let fontSizeStyle = Quill.import("attributors/style/size");
fontSizeStyle.whitelist = ["12px", "14px", "16px", "18px", "20px", "22px", "24px", "26px", "28px", "30px", "32px"];
fontSizeStyle.whitelist = [
"12px",
"14px",
"16px",
"18px",
"20px",
"22px",
"24px",
"26px",
"28px",
"30px",
"32px",
"34px",
"36px",
"38px",
"40px",
"42px"
];
Quill.register(fontSizeStyle, true);
// 自定义Blot
import ImageBlot from "./quill-image";
import Video from "./quill-video";
import TabsBlot from "./quill-tabs";
import DynamicDivBlot from "./quill-detail-div";
Quill.register(Video);
Quill.register(ImageBlot);
Quill.register(TabsBlot);
Quill.register(DynamicDivBlot);
// 基础变量
const { proxy } = getCurrentInstance();
const emit = defineEmits(["update:content", "handleRichTextContentChange"]);
@@ -145,17 +172,20 @@ const tabsData = ref([]);
const tabEditors = ref([]);
// 标题编辑输入框的ref
const editInputRefs = ref([]);
const currentEditingTabsRef = ref(null);
// Props
const props = defineProps({
content: { type: String, default: "" },
readOnly: { type: Boolean, default: false },
fileSizeLimit: { type: Number, default: 10 }
fileSizeLimit: { type: Number, default: 5 }
});
// 主编辑器内容双向绑定
const editorContent = computed({
get: () => props.content,
get: () => {
if (!props.content) return "";
return props.content;
},
set: val => {
emit("update:content", val);
}
@@ -166,6 +196,7 @@ const myQuillEditor = ref(null); // 主编辑器ref
const options = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -199,6 +230,7 @@ const options = reactive({
}
}
},
placeholder: "请输入内容...",
readOnly: props.readOnly
});
@@ -207,6 +239,7 @@ const options = reactive({
const options1 = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -247,7 +280,6 @@ const options1 = reactive({
const handleBeforeUpload = file => {
const fileType = file.type;
file.customUid = generateUUID();
imageListDb.value.push(file);
const validTypes = ["image/jpeg", "image/png", "image/gif", "image/jpg", "image/bmp", "image/webp"];
if (!validTypes.includes(fileType)) {
@@ -262,14 +294,20 @@ const handleBeforeUpload = file => {
imageListDb.value = imageListDb.value.filter(item => item.customUid !== file.customUid);
return false;
}
imageListDb.value.push({
...file,
order: imageListDb.value.length
});
imageListDb.value = [...imageListDb.value].sort((a, b) => a.order - b.order);
console.log(imageListDb.value, "=================value==================");
return true;
};
// 图片上传(保持不变)
const handleHttpUpload = async options => {
let formData = new FormData();
formData.append("image", options.file);
imageList.value.push(options.file);
try {
const result = await uploadImg(formData, routerName.value, options.file.customUid);
@@ -278,12 +316,17 @@ const handleHttpUpload = async options => {
const { imgId } = result;
const fileItem = imageListDb.value.find(item => item.customUid === imgId);
console.log(fileItem, "=fileItem=");
if (fileItem) {
fileItem.serverImgId = imgId;
fileItem.path = data.path;
// 记录完成时间,用于排序
fileItem.completeTime = Date.now();
}
const allFilesUploaded = imageListDb.value.every(item => item.path);
console.log(allFilesUploaded, "=allFilesUploaded=");
if (allFilesUploaded) {
let rawQuillEditor = "";
let quill = "";
@@ -295,18 +338,26 @@ const handleHttpUpload = async options => {
rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
quill = rawQuillEditor.getQuill();
}
// 获取当前光标位置
const selection = quill.getSelection();
const insertPosition = selection ? selection.index : quill.getLength();
imageListDb.value.forEach(item => {
const length = quill.getLength() - 1;
quill.insertEmbed(length, "customImage", {
url: h + item.path,
// 关键修改:按照原始上传顺序排序
// 假设imageListDb中的顺序就是上传顺序
// 或者如果有order属性可以按照order排序
const sortedImages = [...imageListDb.value];
// 按顺序插入图片
sortedImages.forEach((item, index) => {
quill.insertEmbed(insertPosition + index, "customImage", {
url: "https://dev.ow.f2b211.com" + item.path,
id: item.serverImgId || generateUUID()
});
quill.setSelection(length + 1);
});
const finalLength = quill.getLength();
quill.setSelection(finalLength);
// 最终光标定位到最后一张图片后面
const finalPosition = insertPosition + sortedImages.length;
quill.setSelection(finalPosition);
imageList.value = [];
imageListDb.value = [];
@@ -319,34 +370,254 @@ const handleHttpUpload = async options => {
}
};
// // 图片上传(保持不变)
// const handleHttpUpload = async options => {
// let formData = new FormData();
// formData.append("image", options.file);
// imageList.value.push(options.file);
// try {
// const result = await uploadImg(formData, routerName.value, options.file.customUid);
// if (result?.data?.code === 0) {
// const { data } = result.data;
// const { imgId } = result;
// const fileItem = imageListDb.value.find(item => item.customUid === imgId);
// if (fileItem) {
// fileItem.serverImgId = imgId;
// fileItem.path = data.path;
// }
// const allFilesUploaded = imageListDb.value.every(item => item.path);
// if (allFilesUploaded) {
// let rawQuillEditor = "";
// let quill = "";
// if (activeEditor.value === "main") {
// rawQuillEditor = toRaw(myQuillEditor.value);
// quill = rawQuillEditor.getQuill();
// } else {
// const tabIndex = parseInt(activeEditor.value.split("-")[1]);
// rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
// quill = rawQuillEditor.getQuill();
// }
// imageListDb.value.forEach(item => {
// const length = quill.getLength() - 1;
// quill.insertEmbed(length, "customImage", {
// url: item.path,
// id: item.serverImgId || generateUUID()
// });
// quill.setSelection(length + 1);
// });
// const finalLength = quill.getLength();
// quill.setSelection(finalLength);
// imageList.value = [];
// imageListDb.value = [];
// }
// }
// } catch (error) {
// console.error("图片上传失败:", error);
// imageList.value = imageList.value.filter(item => item.customUid !== options.file.customUid);
// imageListDb.value = imageListDb.value.filter(item => item.customUid !== options.file.customUid);
// }
// };
// // // 图片上传(修改插入位置逻辑)
// const handleHttpUpload = async options => {
// let formData = new FormData();
// formData.append("image", options.file);
// // imageList.value.push(options.file);
// try {
// const result = await uploadImg(formData, routerName.value, options.file.customUid);
// if (result?.data?.code === 0) {
// const { data } = result.data;
// const { imgId } = result;
// // imageListDb.value.forEach(item => {
// // if (item.customUid === imgId) {
// // item.serverImgId = imgId;
// // item.path = data.path;
// // console.log(item.path, "================>");
// // }
// // });
// const fileItem = imageListDb.value.find(item => item.customUid === imgId);
// console.log(fileItem, "=fileItem=");
// if (fileItem) {
// fileItem.serverImgId = imgId;
// fileItem.path = data.path;
// }
// const allFilesUploaded = imageListDb.value.every(item => item.path);
// console.log(allFilesUploaded, "=allFilesUploaded=");
// if (allFilesUploaded) {
// let rawQuillEditor = "";
// let quill = "";
// if (activeEditor.value === "main") {
// rawQuillEditor = toRaw(myQuillEditor.value);
// quill = rawQuillEditor.getQuill();
// } else {
// const tabIndex = parseInt(activeEditor.value.split("-")[1]);
// rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
// quill = rawQuillEditor.getQuill();
// }
// // 关键修改:获取当前光标位置(选区起始索引)
// const selection = quill.getSelection();
// // 如果没有选区(光标未激活),默认插入到末尾
// const insertPosition = selection ? selection.index : quill.getLength();
// console.log(imageListDb, "=imageListDb=");
// imageListDb?.value?.forEach(item => {
// // 使用光标位置插入图片
// setTimeout(() => {
// quill.insertEmbed(insertPosition, "customImage", {
// url: "https://dev.ow.f2b211.com" + item.path,
// id: item.serverImgId || generateUUID()
// });
// }, 100);
// // 插入后光标后移一位(避免多张图片重叠插入)
// quill.setSelection(insertPosition + 1);
// });
// // 最终光标定位到最后一张图片后面
// const finalPosition = insertPosition + imageListDb.value.length;
// quill.setSelection(finalPosition);
// imageList.value = [];
// imageListDb.value = [];
// }
// }
// } catch (error) {
// console.error("图片上传失败:", error);
// imageList.value = imageList.value.filter(item => item.customUid !== options.file.customUid);
// imageListDb.value = imageListDb.value.filter(item => item.customUid !== options.file.customUid);
// }
// };
// 视频上传(保持不变)
// const handleVideoUpload = async evt => {
// if (evt.target.files.length === 0) return;
// const formData = new FormData();
// formData.append("video", evt.target.files[0]);
// try {
// let rawQuillEditor = "";
// let quill = "";
// if (activeEditor.value === "main") {
// rawQuillEditor = toRaw(myQuillEditor.value);
// quill = rawQuillEditor.getQuill();
// } else {
// const tabIndex = parseInt(activeEditor.value.split("-")[1]);
// rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
// quill = rawQuillEditor.getQuill();
// }
// let length = quill.selection.savedRange.index;
// const { data } = await uploadVideo(formData);
// quill.insertEmbed(length, "customVideo", {
// url: data.path,
// id: generateUUID()
// });
// uploadFileVideo.value.value = "";
// } catch (error) {
// console.log(error);
// }
// };
// 在<script setup>中替换handleVideoUpload方法
const handleVideoUpload = async evt => {
if (evt.target.files.length === 0) return;
const formData = new FormData();
formData.append("video", evt.target.files[0]);
try {
let rawQuillEditor = "";
let quill = "";
if (activeEditor.value === "main") {
rawQuillEditor = toRaw(myQuillEditor.value);
quill = rawQuillEditor.getQuill();
} else {
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
quill = rawQuillEditor.getQuill();
}
let length = quill.selection.savedRange.index;
const { data } = await uploadVideo(formData);
quill.insertEmbed(length, "customVideo", {
url: h + data.path,
id: generateUUID()
const file = evt.target.files[0];
// 1. 校验视频文件
const maxSize = 150 * 1024 * 1024;
if (file.size > maxSize) {
ElNotification({
title: "文件过大",
message: `视频大小不能超过 ${150}MB`,
type: "warning"
});
uploadFileVideo.value.value = "";
evt.target.value = "";
return;
}
// 2. 生成视频本地URL用于生成封面不上传
const localVideoUrl = URL.createObjectURL(file);
try {
// 4. 并行处理:上传视频 + 生成并上传封面
// 4.1 上传视频到视频服务器
const videoFormData = new FormData();
videoFormData.append("video", file);
const videoRes = await uploadVideo(videoFormData); // 视频上传接口
// 校验视频上传结果(根据你的接口返回格式调整)
if (videoRes?.code !== 0) {
throw new Error(`视频上传失败: ${videoRes?.message || "未知错误"}`);
}
const videoUrl = videoRes.data.path; // 服务器返回的视频URL
console.log(localVideoUrl, "=localVideoUrl=");
// 4.2 生成封面图并上传到图片服务器
const frameBlob = await Video.captureVideoFrame(localVideoUrl);
console.log(frameBlob, "============frameBlob===========");
let coverUrl = "";
if (!frameBlob) return;
// 复用图片上传接口(与图片上传逻辑一致)
const coverFormData = new FormData();
const coverUid = generateUUID(); // 生成唯一ID
// formData.append("image", options.file);
coverFormData.append("image", frameBlob, `cover-${coverUid}.jpg`);
console.log(coverFormData, "=coverFormData=");
// 调用图片上传接口(和普通图片上传用同一个接口)
const coverRes = await uploadImg(coverFormData, routerName.value, coverUid);
// 校验封面上传结果
if (coverRes?.data?.code === 0) {
coverUrl = coverRes.data.data.path; // 服务器返回的封面URL
} else {
console.warn("封面上传失败,使用默认封面");
}
// 5. 将带封面的视频插入编辑器
insertVideoToEditor(videoUrl, coverUrl);
// 6. 上传成功提示
// loading.close();
// ElNotification({
// title: "上传成功",
// message: "视频已添加到编辑器",
// type: "success"
// });
} catch (error) {
console.log(error);
console.log(error, "==============");
} finally {
console.log("======12323232========");
// 清理资源
URL.revokeObjectURL(localVideoUrl); // 释放本地视频URL
evt.target.value = ""; // 重置文件输入框
}
};
// 辅助方法:插入视频到编辑器
const insertVideoToEditor = (videoUrl, coverUrl) => {
// 获取当前活跃的编辑器(主编辑器或标签页编辑器)
let quill;
if (activeEditor.value === "main") {
quill = toRaw(myQuillEditor.value)?.getQuill();
} else {
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
quill = toRaw(tabEditors.value[tabIndex])?.getQuill();
}
if (quill) {
const range = quill.getSelection() || { index: 0 };
// 插入自定义视频组件携带服务器返回的视频URL和封面URL
quill.insertEmbed(range.index, "customVideo", {
url: videoUrl,
poster: coverUrl // 这里使用图片服务器返回的封面URL
});
quill.setSelection(range.index + 1); // 移动光标到视频后
}
};
// 标签页切换事件基于key切换
const handleTabChange = key => {
const tabIndex = tabsData.value.findIndex(item => item.key === key);
@@ -357,6 +628,9 @@ const handleTabChange = key => {
// 标签页增删事件
const handleTabsEdit = (targetKey, action) => {
if (action === "add") {
if (tabsData.value.length > 5) {
return useMsg("error", "标签页已达上限 !");
}
// 新增标签页生成唯一key默认标题初始不处于编辑状态
const newKey = `tab_${generateUUID()}`;
const newIndex = tabsData.value.length;
@@ -440,15 +714,33 @@ const setTabsInfo = () => {
const handleBeforeClose = () => {
setTabsInfo();
};
//确认
// 确认按钮点击事件(修改后)
const handleQR = () => {
const quill = toRaw(myQuillEditor.value)?.getQuill();
if (quill) {
const range = quill.getSelection(true);
quill.insertEmbed(range.index, "tabs", tabsData.value);
quill.setSelection(range.index + 1);
setTabsInfo();
if (!quill) return;
if (!tabsData.value.length) {
return useMsg("error", "标签页内容为空 !");
}
const range = quill.getSelection(true);
// 判断是否是编辑已有标签页(通过 currentEditingTabsRef 是否有值)
if (currentEditingTabsRef.value) {
// 1. 编辑模式:更新原有标签页组件
const blot = currentEditingTabsRef.value;
// 更新 blot 的数据(触发 DOM 更新)
blot.updateContents(tabsData.value); // 需要在 TabsBlot 中添加 updateContents 方法
// 清除编辑状态标记
currentEditingTabsRef.value = null;
} else {
// 2. 新增模式:插入新的标签页组件
quill.insertEmbed(range.index, "tabs", tabsData.value);
// 关键:在标签页前方插入一个空段落(确保顶部有空间)
// quill.insertText(range.index, "\n"); // 插入换行
quill.setSelection(range.index + 1);
quill.insertText(range.index, "\n"); // 插入换行
}
// 关闭弹窗并清空临时数据
setTabsInfo();
};
//取消
const handleQX = () => {
@@ -462,11 +754,87 @@ const initTitle = () => {
if (tip) tip.setAttribute("title", item.title);
});
};
// 定义 loadTabsDataToEditor 函数
const loadTabsDataToEditor = tabs => {
// 清空现有数据
tabsData.value = [];
// 转换原始标签数据为编辑所需格式添加key和编辑状态
tabs.forEach((tab, index) => {
tabsData.value.push({
key: `tab_${generateUUID()}`, // 生成唯一key
title: tab.title || `标签${index + 1}`, // 避免空标题
content: tab.content || "", // 标签页内容
isEditing: false // 编辑状态标记
});
});
// 激活第一个标签页(如果有数据)
nextTick(() => {
if (tabsData.value.length > 0) {
activeName.value = tabsData.value[0].key;
activeEditor.value = "tab-0";
}
});
};
// const cleanEmptyTags = container => {
// if (!container) return;
// // 获取所有 <p> 标签
// const pTags = container.querySelectorAll("p");
// // 只清理真正的空标签(不含任何内容,包括<br>
// Array.from(pTags).forEach(pTag => {
// // 判断标准:
// // 1. 完全没有子节点
// // 2. 或innerHTML为空字符串
// const isCompletelyEmpty = pTag.childNodes.length === 0 || pTag.innerHTML.trim() === "";
// if (isCompletelyEmpty) {
// // 特殊处理:保留首尾空标签,避免编辑器异常
// const isFirstOrLast = pTag === container.firstElementChild || pTag === container.lastElementChild;
// if (isFirstOrLast) {
// pTag.innerHTML = ""; // 清空内容
// } else {
// pTag.remove(); // 移除中间的纯空标签
// }
// }
// });
// };
// // 触发清理的函数(针对所有编辑器)
// const handleCleanEmptyTags = () => {
// // 处理主编辑器
// const mainEditor = document.querySelector("#mainEditor .ql-editor");
// console.log(mainEditor, "=mainEditor=");
// if (mainEditor) cleanEmptyTags(mainEditor);
// };
onMounted(() => {
initTitle();
// 监听编辑按钮点击事件
const editorEl = document.querySelector(".ql-editor");
if (editorEl) {
editorEl.addEventListener("edit-tabs", e => {
const tabsData = TabsBlot.value(e.detail.blot.domNode);
if (tabsData.length > 0) {
// 保存当前编辑的标签页引用
currentEditingTabsRef.value = e.detail.blot;
// 加载数据到弹窗
loadTabsDataToEditor(tabsData);
// 显示弹窗
outerVisible.value = true;
}
});
}
// 等待编辑器首次渲染完成
// setTimeout(handleCleanEmptyTags, 300);
});
// 监听内容变化,重新清理空标签
// watch(editorContent, () => {
// nextTick(() => {
// setTimeout(handleCleanEmptyTags, 100); // 延迟确保 Quill 已重新渲染
// });
// });
defineExpose({
clearEditor: () => {
const quill = toRaw(myQuillEditor.value)?.getQuill();
@@ -488,63 +856,105 @@ defineExpose({
user-select: text !important;
}
/* 标签页样式 */
// /* 标签页样式 */
.quill-tabs {
margin: 15px 0;
overflow: hidden;
border: 1px solid #dddddd;
border-radius: 4px;
}
/* 用伪元素添加图标(可替换为自己的图标) */
// /* 用伪元素添加图标(可替换为自己的图标) */
.ql-tabs::before {
font-size: 16px;
content: "T"; /* 用 emoji 或字体图标 */
}
.quill-tab-list {
display: flex;
background-color: #f8f9fa;
border-bottom: 1px solid #dddddd;
}
.quill-tab-button {
padding: 10px 15px;
font-weight: 500;
cursor: pointer;
background: transparent;
border: none;
transition: background-color 0.2s;
}
.quill-tab-button.active {
color: #007bff;
background-color: white;
border-bottom: 2px solid #007bff;
}
.quill-tab-content-list {
padding: 15px;
}
.quill-tab-content {
display: none;
}
.quill-tab-content.active {
display: block;
.title-input {
width: 100px;
margin: -2px 0; /* 与标签对齐 */
}
/* 标签页标题编辑样式 */
.tab-title-edit {
display: flex;
align-items: center;
.title-text {
display: inline-block;
min-width: 60px;
padding: 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.title-input {
width: 100px;
margin: -2px 0; /* 与标签对齐 */
}
/* 详情样式 */
.o_detail_all {
overflow: hidden;
text-align: center;
background-color: #ffffff;
}
.o_detail_title {
margin-top: 2vw;
margin-bottom: 1.25vw;
overflow: hidden;
font-size: 2.25em;
font-weight: 600;
line-height: 1.2em;
color: #101010;
text-align: center;
background-color: #ffffff;
}
.o_detail_small {
margin-bottom: 0.7vw;
font-size: 1.5em;
color: #333333;
}
.o_detail_text {
width: 80%;
margin-right: auto;
margin-bottom: 0.7vw;
margin-left: auto;
font-size: 1.125em;
line-height: 1.5em;
color: #737373;
}
.products_des {
width: 100%;
margin-bottom: 50px;
}
.products_des img {
width: 100%;
}
.de_t_n {
font-size: 1.5em;
color: #333333;
}
.detail_title {
padding: 2% 0;
text-align: center;
}
.detail_title p {
line-height: 2em;
}
.detail_con_a {
margin: auto;
overflow: hidden;
}
.lj_detail_text,
.lj_detail_texts {
font-size: 0.875em;
}
.lj_detail_text p {
padding: 0.5% 0;
line-height: 1.6em;
}
/* seo-pro */
.seo-pro h3 {
margin: 2% 0 1%;
font-size: 1.5em;
font-weight: 400;
line-height: 1.2;
color: #333333;
text-align: center;
}
.seo-pro p {
margin: 0 0 11px;
text-align: center;
}
.seo-pro a {
color: #333333;
text-decoration: none;
}
.sa_blue,
.sa_blue a,
.seo-pro a:hover {
color: #009fdf;
}
</style>

View File

@@ -0,0 +1,49 @@
// quill-dynamic-div.js
import { Quill } from "@vueup/vue-quill";
const Block = Quill.import("blots/block");
class DynamicDivBlot extends Block {
// 从 DOM 节点创建 Blot 时,保留所有属性和类名
static create(value) {
const node = super.create();
// 如果是从数据恢复value 存在),还原类名
if (value?.className) {
node.className = value.className; // 直接设置完整类名字符串(支持多类名)
}
// 保留其他属性(如 id、data-* 等,按需添加)
if (value?.attrs) {
Object.keys(value.attrs).forEach(key => {
node.setAttribute(key, value.attrs[key]);
});
}
return node;
}
// 从 Blot 提取值(序列化时用),保留所有类名和属性
static value(node) {
return {
className: node.className, // 保留完整类名(如 "o_detail_all big"
attrs: Array.from(node.attributes).reduce((attrs, attr) => {
attrs[attr.name] = attr.value;
return attrs;
}, {}) // 保留所有属性
};
}
// 格式化时识别所有 div 标签
static formats(domNode) {
// 只要是 div 标签,就返回其类名和属性(确保 Quill 不清理)
return {
className: domNode.className,
attrs: Array.from(domNode.attributes).reduce((attrs, attr) => {
attrs[attr.name] = attr.value;
return attrs;
}, {})
};
}
}
DynamicDivBlot.blotName = "dynamicDiv"; // 唯一标识
DynamicDivBlot.tagName = "div"; // 处理所有 div 标签
export default DynamicDivBlot;

View File

@@ -11,8 +11,6 @@ class ImageBlot extends BlockEmbed {
}
// 允许通过键盘删除
deleteAt(index, length) {
console.log(index, length, "===============>");
console.log("===========super===========");
super.deleteAt(index, length);
}
static value(node) {

View File

@@ -0,0 +1,25 @@
import { Quill } from "@vueup/vue-quill";
let BlockEmbed = Quill.import("blots/block/embed");
class ImageBlot extends BlockEmbed {
static create(value) {
let node = super.create();
node.setAttribute("src", value.url);
node.setAttribute("id", value.id);
console.log("图片信息", node);
return node;
}
// 允许通过键盘删除
deleteAt(index, length) {
super.deleteAt(index, length);
}
static value(node) {
return {
url: node.getAttribute("src"),
id: node.getAttribute("id")
};
}
}
ImageBlot.blotName = "customImage";
ImageBlot.tagName = "img";
export default ImageBlot;

View File

@@ -28,18 +28,88 @@ class TabsBlot extends BlockEmbed {
`
);
// 标签栏
// 标签栏滚动容器 - 优化高度计算
const tabScrollContainer = document.createElement("div");
tabScrollContainer.className = "m-quill-tab-scroll-container";
tabScrollContainer.setAttribute(
"style",
`
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
overflow-y: hidden;
overflow-x: auto;
height: auto;
`
);
// height: auto; /* 自动高度 */
// Chrome, Safari 隐藏滚动条
tabScrollContainer.style.overflow = "auto";
tabScrollContainer.style.webkitOverflowScrolling = "touch";
tabScrollContainer.style.scrollbarWidth = "none";
tabScrollContainer.style.msOverflowStyle = "none";
// 标签栏 - 保持原有样式不变
const tabList = document.createElement("div");
tabList.className = "m-quill-tab-list";
tabList.setAttribute(
"style",
`
display: flex;
border-bottom: 1px solid #dddddd;
border-bottom: 2px solid #dddddd;
min-width: max-content; /* 确保内容撑开容器 */
`
);
// 内容区
// 生成标签按钮 - 优化内边距计算
tabs.forEach((tab, index) => {
const btn = document.createElement("button");
btn.className = `m-quill-tab-button`;
btn.setAttribute("data-index", index);
btn.textContent = tab.title;
btn.setAttribute(
"style",
`
font-weight: 900;
color: #8f9099;
cursor: pointer;
background: transparent;
border: none;
padding-bottom:14px;
margin-right:3%; /* 增大间距 */
cursor: pointer;
white-space:nowrap;
font-size:16px;
${index === 0 ? "color: #1f2635; border-bottom: 3px solid #537CD8;font-size:16px;" : ""}
`
);
tabList.appendChild(btn);
});
// 编辑按钮 - 保持原有样式不变
const editBtn = document.createElement("button");
editBtn.className = "m-quill-tab-edit-btn";
editBtn.innerHTML = "编辑";
editBtn.setAttribute("data-action", "edit");
editBtn.setAttribute(
"style",
`
padding: 10px;
margin-left: auto;
color: #606266;
cursor: pointer;
width:60px;
padding-left:20px;
background: transparent;
border: none;
display:block;
`
);
tabList.appendChild(editBtn);
// 内容区 - 保持原有样式不变
const contentList = document.createElement("div");
contentList.className = "m-quill-tab-content-list";
contentList.setAttribute(
@@ -49,31 +119,8 @@ class TabsBlot extends BlockEmbed {
`
);
// 生成标签按钮和内容面板
// 生成内容面板 - 保持原有样式不变
tabs.forEach((tab, index) => {
// 标签按钮
const btn = document.createElement("button");
btn.className = `m-quill-tab-button`;
btn.setAttribute("data-index", index);
btn.textContent = tab.title;
btn.setAttribute(
"style",
`
padding: 10px 15px;
font-weight: 900;
color: #8f9099;
cursor: pointer;
background: transparent;
border: none;
margin-right: 60px;
cursor: pointer;
font-size:16px;
${index === 0 ? "color: #1f2635; border-bottom: 3px solid #537CD8;font-size:16px;" : ""}
`
);
tabList.appendChild(btn);
// 内容面板 - 关键修改:设置为可编辑
const panel = document.createElement("div");
panel.className = `m-quill-tab-content`;
panel.setAttribute("data-index", index);
@@ -85,88 +132,74 @@ class TabsBlot extends BlockEmbed {
min-height: 50px;
`
);
panel.contentEditable = "false"; // 内容面板可编辑
panel.contentEditable = "false";
contentList.appendChild(panel);
});
// 编辑按钮
const editBtn = document.createElement("button");
editBtn.className = "m-quill-tab-edit-btn";
editBtn.innerHTML = "编辑";
editBtn.setAttribute("data-action", "edit");
editBtn.setAttribute(
"style",
`
// 组装结构
tabScrollContainer.appendChild(tabList);
node.appendChild(tabScrollContainer); // 滚动容器添加到主节点
node.appendChild(contentList); // 内容区
padding: 10px;
margin-left: auto;
color: #606266;
cursor: pointer;
background: transparent;
border: none;
display:block;
`
);
// display: flex;
//align-items: center;
// editBtn.onmouseover = () => {
// editBtn.style.color = "#007bff";
// };
// editBtn.onmouseout = () => {
// editBtn.style.color = "#606266";
// };
tabList.appendChild(editBtn);
// 标签页切换逻辑
// 标签页切换逻辑 - 保持原有逻辑不变
const scriptTag = document.createElement("script");
// 修改 scriptTag.textContent 中的逻辑
scriptTag.textContent = `
(function() {
const container = document.currentScript.parentElement;
const isAdmin = window.location.pathname.includes('/admin');
const editBtn1 = container.querySelector('.m-quill-tab-edit-btn');
(function() {
const container = document.currentScript.parentElement;
const isAdmin = window.location.pathname.includes('/admin');
const editBtn1 = container.querySelector('.m-quill-tab-edit-btn');
// 仅在非管理系统(文章网站)隐藏编辑按钮,管理系统保持显示
if (!isAdmin && editBtn1) {
editBtn1.style.display = 'none'; // 文章网站隐藏按钮
} else if (isAdmin && editBtn1) {
editBtn1.style.display = 'block'; // 管理系统强制显示按钮
editBtn1.style.width='60px';
editBtn1.style.minWidth='60px';
}
// 仅在非管理系统(文章网站)隐藏编辑按钮,管理系统保持显示
if (!isAdmin && editBtn1) {
editBtn1.style.display = 'none'; // 文章网站隐藏按钮
} else if (isAdmin && editBtn1) {
editBtn1.style.display = 'block'; // 管理系统强制显示按钮
}
// 非管理系统才执行标签切换逻辑(管理系统不执行)
if (!isAdmin) {
const tabButtons = container.querySelectorAll('.m-quill-tab-button:not([data-action])');
const contentPanels = container.querySelectorAll('.m-quill-tab-content');
tabButtons.forEach(btn => {
btn.addEventListener('click', function() {
const index = parseInt(this.dataset.index);
tabButtons.forEach((b, i) => {
b.setAttribute('style', \`
padding: 10px 15px;
font-weight: 900;
cursor: pointer;
background: transparent;
font-size:16px;
margin-right: 60px;
color: #8f9099;
border: none;
\${i === index ?
'color: #1f2635;border-bottom: 3px solid #537CD8;font-size:16px;' :
''
}
\`);
});
contentPanels.forEach((panel, i) => {
panel.style.display = i === index ? 'block' : 'none';
});
// 非管理系统才执行标签切换逻辑(管理系统不执行)
if (!isAdmin) {
const tabButtons = container.querySelectorAll('.m-quill-tab-button:not([data-action])');
const contentPanels = container.querySelectorAll('.m-quill-tab-content');
tabButtons.forEach(btn => {
btn.addEventListener('click', function() {
const index = parseInt(this.dataset.index);
tabButtons.forEach((b, i) => {
b.setAttribute('style', \`
font-weight: 900;
cursor: pointer;
background: transparent;
font-size:16px;
white-space:nowrap;
padding-bottom:14px;
margin-right:3%;
color: #8f9099;
border: none;
\${i === index ?
'color: #1f2635;border-bottom: 3px solid #537CD8;font-size:16px;' :
''
}
\`);
});
contentPanels.forEach((panel, i) => {
panel.style.display = i === index ? 'block' : 'none';
});
// 添加滚动逻辑到自执行函数中
const scrollContainer = container.querySelector(".m-quill-tab-scroll-container");
const activeBtn = tabButtons[index];
if (scrollContainer && activeBtn) {
activeBtn.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center"
});
}
});
}
})();
`;
});
}
})();
`;
node.appendChild(tabList);
node.appendChild(contentList);
node.appendChild(scriptTag);
node.setAttribute("contenteditable", "false");
return node;
@@ -177,15 +210,12 @@ class TabsBlot extends BlockEmbed {
this.eventBoundElements = new WeakMap();
}
// 移除 eventBoundElements 依赖,直接重新绑定事件(避免弱映射导致的问题)
// 编辑按钮事件 - 保持原有逻辑不变
const editBtn = this.domNode.querySelector(".m-quill-tab-edit-btn");
if (editBtn) {
// 先移除旧事件,避免重复绑定
editBtn.removeEventListener("click", this.handleEditClick);
// 绑定新事件(使用箭头函数确保 this 指向正确)
this.handleEditClick = e => {
e.stopPropagation();
console.log("1232323");
this.domNode.dispatchEvent(
new CustomEvent("edit-tabs", {
bubbles: true,
@@ -196,7 +226,7 @@ class TabsBlot extends BlockEmbed {
editBtn.addEventListener("click", this.handleEditClick);
}
// 标签切换事件
// 标签切换事件 - 保持原有逻辑不变
const tabButtons = this.domNode.querySelectorAll(".m-quill-tab-button:not([data-action])");
tabButtons.forEach(btn => {
if (!this.eventBoundElements.has(btn)) {
@@ -209,9 +239,8 @@ class TabsBlot extends BlockEmbed {
});
}
// 增强版删除键处理
// 增强版删除键处理 - 保持原有逻辑不变
bindDeleteKeyEvent() {
// 阻止从外部删除整个组件
this.domNode.addEventListener(
"keydown",
e => {
@@ -222,30 +251,26 @@ class TabsBlot extends BlockEmbed {
const range = selection.getRangeAt(0);
const parentBlock = this.domNode;
// 判断光标是否在组件内部
const isInside = parentBlock.contains(range.commonAncestorContainer);
// 如果光标在组件外部,阻止删除
if (!isInside) {
e.preventDefault();
return;
}
// 检查是否选中了整个组件
if (
range.startContainer === parentBlock &&
range.endContainer === parentBlock &&
range.startOffset === 0 &&
range.endOffset >= parentBlock.childNodes.length
) {
e.preventDefault(); // 阻止删除整个组件
e.preventDefault();
}
}
},
true
); // 使用捕获阶段
);
// 标签栏禁止编辑
const tabList = this.domNode.querySelector(".m-quill-tab-list");
if (tabList) {
tabList.querySelectorAll("*").forEach(el => {
@@ -258,17 +283,19 @@ class TabsBlot extends BlockEmbed {
const buttons = this.domNode.querySelectorAll(".m-quill-tab-button:not([data-action])");
const panels = this.domNode.querySelectorAll(".m-quill-tab-content");
// 保持原有样式逻辑不变
buttons.forEach((btn, i) => {
btn.setAttribute(
"style",
`
padding: 10px 15px;
font-weight: 900;
cursor: pointer;
background: transparent;
border: none;
font-size:16px;
margin-right: 60px;
padding-bottom:14px;
margin-right:3%;
white-space:nowrap;
color: #8f9099;
border-bottom: 3px solid transparent;
${i === index ? "color: #1f2635;border-bottom: 3px solid #537CD8;font-size:16px;" : ""}
@@ -279,6 +306,17 @@ class TabsBlot extends BlockEmbed {
panels.forEach((panel, i) => {
panel.style.display = i === index ? "block" : "none";
});
// 滚动到当前选中的标签
const scrollContainer = this.domNode.querySelector(".m-quill-tab-scroll-container");
const activeBtn = buttons[index];
if (scrollContainer && activeBtn) {
activeBtn.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center"
});
}
}
static value(node) {
@@ -301,26 +339,23 @@ class TabsBlot extends BlockEmbed {
const scriptTag = this.domNode.querySelector("script");
if (scriptTag) {
const newScript = document.createElement("script");
newScript.textContent = scriptTag.textContent;
scriptTag.parentNode.replaceChild(newScript, scriptTag);
}
this.bindEvents();
this.bindDeleteKeyEvent(); // 重新绑定删除事件
this.bindDeleteKeyEvent();
}
getValue() {
return TabsBlot.value(this.domNode);
}
// 更新标签页数据(编辑后更新 DOM
// 更新标签页数据 - 保持原有逻辑不变
updateContents(tabs) {
// 清空原有内容
const contentList = this.domNode.querySelector(".m-quill-tab-content-list");
const tabList = this.domNode.querySelector(".m-quill-tab-list");
// 保留编辑按钮(如果需要)
const editBtn = this.domNode.querySelector(".m-quill-tab-edit-btn");
// 清空标签栏和内容区(保留编辑按钮)
Array.from(tabList.children).forEach(child => {
if (!child.classList.contains("m-quill-tab-edit-btn")) {
child.remove();
@@ -328,9 +363,7 @@ class TabsBlot extends BlockEmbed {
});
contentList.innerHTML = "";
// 重新渲染标签页(复用 create 方法中的逻辑)
tabs.forEach((tab, index) => {
// 重建标签按钮
const btn = document.createElement("button");
btn.className = "m-quill-tab-button";
btn.setAttribute("data-index", index);
@@ -338,20 +371,20 @@ class TabsBlot extends BlockEmbed {
btn.setAttribute(
"style",
`
padding: 10px 15px;
font-weight: 900;
color: #8f9099;
cursor: pointer;
background: transparent;
border: none;
margin-right: 60px;
font-size:16px;
${index === 0 ? "color: #1f2635; border-bottom: 3px solid #537CD8;font-size:16px;" : ""}
`
font-weight: 900;
color: #8f9099;
cursor: pointer;
background: transparent;
border: none;
padding-bottom:14px;
margin-right:3%;
white-space:nowrap;
font-size:16px;
${index === 0 ? "color: #1f2635; border-bottom: 3px solid #537CD8;font-size:16px;" : ""}
`
);
tabList.insertBefore(btn, editBtn); // 插入到编辑按钮前
tabList.insertBefore(btn, editBtn);
// 重建内容面板
const panel = document.createElement("div");
panel.className = "m-quill-tab-content";
panel.setAttribute("data-index", index);
@@ -359,15 +392,14 @@ class TabsBlot extends BlockEmbed {
panel.setAttribute(
"style",
`
display: ${index === 0 ? "block" : "none"};
min-height: 50px;
`
display: ${index === 0 ? "block" : "none"};
min-height: 50px;
`
);
panel.contentEditable = "false";
contentList.appendChild(panel);
});
// 重新绑定事件(确保切换功能正常)
this.bindEvents();
}
}

View File

@@ -5,7 +5,7 @@ const BlockEmbed = Quill.import("blots/block/embed");
class TabsBlot extends BlockEmbed {
static blotName = "tabs";
static tagName = "div";
static className = "quill-tabs";
static className = "m-quill-tabs";
constructor(domNode) {
super(domNode);
@@ -23,27 +23,100 @@ class TabsBlot extends BlockEmbed {
`
margin: 15px 0;
overflow: hidden;
border: 1px solid #dddddd;
border-radius: 4px;
position: relative;
`
);
// 标签栏
// 标签栏滚动容器 - 添加隐藏滚动条样式
const tabScrollContainer = document.createElement("div");
tabScrollContainer.className = "m-quill-tab-scroll-container";
tabScrollContainer.setAttribute(
"style",
`
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch; /* 增强移动端滚动体验 */
scrollbar-width: none; /* Firefox 隐藏滚动条 */
-ms-overflow-style: none; /* IE 10+ 隐藏滚动条 */
`
);
// Chrome, Safari 隐藏滚动条
tabScrollContainer.style.overflow = "auto";
tabScrollContainer.style.webkitOverflowScrolling = "touch";
tabScrollContainer.style.scrollbarWidth = "none";
tabScrollContainer.style.msOverflowStyle = "none";
// 关键:隐藏滚动条但保留功能
tabScrollContainer.innerHTML = `
<style>
.m-quill-tab-scroll-container::-webkit-scrollbar {
display: none; /* Chrome, Safari 隐藏滚动条 */
}
</style>
`;
// 标签栏 - 保持原有样式不变
const tabList = document.createElement("div");
tabList.className = "quill-tab-list";
tabList.className = "m-quill-tab-list";
tabList.setAttribute(
"style",
`
display: flex;
background-color: #f8f9fa;
border-bottom: 1px solid #dddddd;
min-width: max-content; /* 确保内容撑开容器 */
`
);
// 内容区
// 生成标签按钮 - 保持原有样式不变
tabs.forEach((tab, index) => {
const btn = document.createElement("button");
btn.className = `m-quill-tab-button`;
btn.setAttribute("data-index", index);
btn.textContent = tab.title;
btn.setAttribute(
"style",
`
padding: 1%;
font-weight: 900;
color: #8f9099;
cursor: pointer;
background: transparent;
border: none;
margin-right: 1%;
cursor: pointer;
font-size:16px;
${index === 0 ? "color: #1f2635; border-bottom: 3px solid #537CD8;font-size:16px;" : ""}
`
);
tabList.appendChild(btn);
});
// 编辑按钮 - 保持原有样式不变
const editBtn = document.createElement("button");
editBtn.className = "m-quill-tab-edit-btn";
editBtn.innerHTML = "编辑";
editBtn.setAttribute("data-action", "edit");
editBtn.setAttribute(
"style",
`
padding: 10px;
margin-left: auto;
color: #606266;
cursor: pointer;
width:50px;
background: transparent;
border: none;
display:block;
`
);
tabList.appendChild(editBtn);
// 内容区 - 保持原有样式不变
const contentList = document.createElement("div");
contentList.className = "quill-tab-content-list";
contentList.className = "m-quill-tab-content-list";
contentList.setAttribute(
"style",
`
@@ -51,106 +124,77 @@ class TabsBlot extends BlockEmbed {
`
);
// 生成标签按钮和内容面板
// 生成内容面板 - 保持原有样式不变
tabs.forEach((tab, index) => {
// 标签按钮
const btn = document.createElement("button");
btn.className = `quill-tab-button ${index === 0 ? "active" : ""}`;
btn.setAttribute("data-index", index);
btn.textContent = tab.title;
btn.setAttribute(
"style",
`
padding: 10px 15px;
font-weight: 500;
cursor: pointer;
background: transparent;
border: none;
transition: background-color 0.2s;
${index === 0 ? "color: #007bff; background-color: white; border-bottom: 2px solid #007bff;" : ""}
`
);
tabList.appendChild(btn);
// 内容面板 - 关键修改:设置为可编辑
const panel = document.createElement("div");
panel.className = `quill-tab-content ${index === 0 ? "active" : ""}`;
panel.className = `m-quill-tab-content`;
panel.setAttribute("data-index", index);
panel.innerHTML = tab.content;
panel.setAttribute(
"style",
`
display: ${index === 0 ? "block" : "none"};
min-height: 50px; /* 确保有编辑区域 */
min-height: 50px;
`
);
panel.contentEditable = "true"; // 内容面板可编辑
panel.contentEditable = "false";
contentList.appendChild(panel);
});
// 编辑按钮
const editBtn = document.createElement("button");
editBtn.className = "quill-tab-edit-btn";
editBtn.innerHTML = "编辑";
editBtn.setAttribute("data-action", "edit");
editBtn.setAttribute(
"style",
`
display: flex;
align-items: center;
padding: 10px;
margin-left: auto;
color: #606266;
cursor: pointer;
background: transparent;
border: none;
`
);
editBtn.onmouseover = () => {
editBtn.style.color = "#007bff";
};
editBtn.onmouseout = () => {
editBtn.style.color = "#606266";
};
tabList.appendChild(editBtn);
// 标签页切换逻辑
// 组装结构
tabScrollContainer.appendChild(tabList);
node.appendChild(tabScrollContainer); // 滚动容器添加到主节点
node.appendChild(contentList); // 内容区
// 标签页切换逻辑 - 保持原有逻辑不变
const scriptTag = document.createElement("script");
scriptTag.textContent = `
(function() {
const container = document.currentScript.parentElement;
const tabButtons = container.querySelectorAll('.quill-tab-button:not([data-action])');
const contentPanels = container.querySelectorAll('.quill-tab-content');
tabButtons.forEach(btn => {
btn.addEventListener('click', function() {
const index = parseInt(this.dataset.index);
tabButtons.forEach((b, i) => {
b.setAttribute('style', \`
padding: 10px 15px;
font-weight: 500;
cursor: pointer;
background: transparent;
border: none;
transition: background-color 0.2s;
\${i === index ?
'color: #007bff; background-color: white; border-bottom: 2px solid #007bff;' :
''
}
\`);
});
const isAdmin = window.location.pathname.includes('/admin');
const editBtn1 = container.querySelector('.m-quill-tab-edit-btn');
contentPanels.forEach((panel, i) => {
panel.style.display = i === index ? 'block' : 'none';
// 仅在非管理系统(文章网站)隐藏编辑按钮,管理系统保持显示
if (!isAdmin && editBtn1) {
editBtn1.style.display = 'none'; // 文章网站隐藏按钮
} else if (isAdmin && editBtn1) {
editBtn1.style.display = 'block'; // 管理系统强制显示按钮
}
// 非管理系统才执行标签切换逻辑(管理系统不执行)
if (!isAdmin) {
const tabButtons = container.querySelectorAll('.m-quill-tab-button:not([data-action])');
const contentPanels = container.querySelectorAll('.m-quill-tab-content');
tabButtons.forEach(btn => {
btn.addEventListener('click', function() {
const index = parseInt(this.dataset.index);
tabButtons.forEach((b, i) => {
b.setAttribute('style', \`
padding: 1%;
font-weight: 900;
cursor: pointer;
background: transparent;
font-size:16px;
margin-right: 1%;
color: #8f9099;
border: none;
\${i === index ?
'color: #1f2635;border-bottom: 3px solid #537CD8;font-size:16px;' :
''
}
\`);
});
contentPanels.forEach((panel, i) => {
panel.style.display = i === index ? 'block' : 'none';
});
});
});
});
}
})();
`;
// 组装结构 - 关键修改移除contenteditable="false"
node.appendChild(tabList);
node.appendChild(contentList);
node.appendChild(scriptTag);
node.setAttribute("contenteditable", "false");
return node;
}
@@ -159,10 +203,11 @@ class TabsBlot extends BlockEmbed {
this.eventBoundElements = new WeakMap();
}
// 编辑按钮事件
const editBtn = this.domNode.querySelector(".quill-tab-edit-btn");
if (editBtn && !this.eventBoundElements.has(editBtn)) {
editBtn.addEventListener("click", e => {
// 编辑按钮事件 - 保持原有逻辑不变
const editBtn = this.domNode.querySelector(".m-quill-tab-edit-btn");
if (editBtn) {
editBtn.removeEventListener("click", this.handleEditClick);
this.handleEditClick = e => {
e.stopPropagation();
this.domNode.dispatchEvent(
new CustomEvent("edit-tabs", {
@@ -170,12 +215,12 @@ class TabsBlot extends BlockEmbed {
detail: { blot: this }
})
);
});
this.eventBoundElements.set(editBtn, true);
};
editBtn.addEventListener("click", this.handleEditClick);
}
// 标签切换事件
const tabButtons = this.domNode.querySelectorAll(".quill-tab-button:not([data-action])");
// 标签切换事件 - 保持原有逻辑不变
const tabButtons = this.domNode.querySelectorAll(".m-quill-tab-button:not([data-action])");
tabButtons.forEach(btn => {
if (!this.eventBoundElements.has(btn)) {
btn.addEventListener("click", () => {
@@ -187,9 +232,8 @@ class TabsBlot extends BlockEmbed {
});
}
// 增强版删除键处理
// 增强版删除键处理 - 保持原有逻辑不变
bindDeleteKeyEvent() {
// 阻止从外部删除整个组件
this.domNode.addEventListener(
"keydown",
e => {
@@ -200,31 +244,27 @@ class TabsBlot extends BlockEmbed {
const range = selection.getRangeAt(0);
const parentBlock = this.domNode;
// 判断光标是否在组件内部
const isInside = parentBlock.contains(range.commonAncestorContainer);
// 如果光标在组件外部,阻止删除
if (!isInside) {
e.preventDefault();
return;
}
// 检查是否选中了整个组件
if (
range.startContainer === parentBlock &&
range.endContainer === parentBlock &&
range.startOffset === 0 &&
range.endOffset >= parentBlock.childNodes.length
) {
e.preventDefault(); // 阻止删除整个组件
e.preventDefault();
}
}
},
true
); // 使用捕获阶段
);
// 标签栏禁止编辑
const tabList = this.domNode.querySelector(".quill-tab-list");
const tabList = this.domNode.querySelector(".m-quill-tab-list");
if (tabList) {
tabList.querySelectorAll("*").forEach(el => {
el.contentEditable = "false";
@@ -233,20 +273,24 @@ class TabsBlot extends BlockEmbed {
}
selectTab(index) {
const buttons = this.domNode.querySelectorAll(".quill-tab-button:not([data-action])");
const panels = this.domNode.querySelectorAll(".quill-tab-content");
const buttons = this.domNode.querySelectorAll(".m-quill-tab-button:not([data-action])");
const panels = this.domNode.querySelectorAll(".m-quill-tab-content");
// 保持原有样式逻辑不变
buttons.forEach((btn, i) => {
btn.setAttribute(
"style",
`
padding: 10px 15px;
font-weight: 500;
padding: 1%;
font-weight: 900;
cursor: pointer;
background: transparent;
border: none;
transition: background-color 0.2s;
${i === index ? "color: #007bff; background-color: white; border-bottom: 2px solid #007bff;" : ""}
font-size:16px;
margin-right: 1%;
color: #8f9099;
border-bottom: 3px solid transparent;
${i === index ? "color: #1f2635;border-bottom: 3px solid #537CD8;font-size:16px;" : ""}
`
);
});
@@ -254,21 +298,32 @@ class TabsBlot extends BlockEmbed {
panels.forEach((panel, i) => {
panel.style.display = i === index ? "block" : "none";
});
// 滚动到当前选中的标签
const scrollContainer = this.domNode.querySelector(".m-quill-tab-scroll-container");
const activeBtn = buttons[index];
if (scrollContainer && activeBtn) {
activeBtn.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center"
});
}
}
static value(node) {
const tabs = [];
const buttons = node.querySelectorAll(".quill-tab-button:not([data-action])");
const panels = node.querySelectorAll(".quill-tab-content");
const buttons = node.querySelectorAll(".m-quill-tab-button:not([data-action])");
const panels = node.querySelectorAll(".m-quill-tab-content");
buttons.forEach((btn, i) => {
tabs.push({
title: btn.textContent,
content: panels[i].innerHTML
content: panels[i]?.innerHTML || ""
});
});
return { tabs };
return tabs;
}
update(mutations, context) {
@@ -280,7 +335,63 @@ class TabsBlot extends BlockEmbed {
scriptTag.parentNode.replaceChild(newScript, scriptTag);
}
this.bindEvents();
this.bindDeleteKeyEvent(); // 重新绑定删除事件
this.bindDeleteKeyEvent();
}
getValue() {
return TabsBlot.value(this.domNode);
}
// 更新标签页数据 - 保持原有逻辑不变
updateContents(tabs) {
const contentList = this.domNode.querySelector(".m-quill-tab-content-list");
const tabList = this.domNode.querySelector(".m-quill-tab-list");
const editBtn = this.domNode.querySelector(".m-quill-tab-edit-btn");
Array.from(tabList.children).forEach(child => {
if (!child.classList.contains("m-quill-tab-edit-btn")) {
child.remove();
}
});
contentList.innerHTML = "";
tabs.forEach((tab, index) => {
const btn = document.createElement("button");
btn.className = "m-quill-tab-button";
btn.setAttribute("data-index", index);
btn.textContent = tab.title;
btn.setAttribute(
"style",
`
padding: 1%;
font-weight: 900;
color: #8f9099;
cursor: pointer;
background: transparent;
border: none;
margin-right: 1%;
font-size:16px;
${index === 0 ? "color: #1f2635; border-bottom: 3px solid #537CD8;font-size:16px;" : ""}
`
);
tabList.insertBefore(btn, editBtn);
const panel = document.createElement("div");
panel.className = "m-quill-tab-content";
panel.setAttribute("data-index", index);
panel.innerHTML = tab.content;
panel.setAttribute(
"style",
`
display: ${index === 0 ? "block" : "none"};
min-height: 50px;
`
);
panel.contentEditable = "false";
contentList.appendChild(panel);
});
this.bindEvents();
}
}

View File

@@ -1,180 +0,0 @@
import { Quill } from "@vueup/vue-quill";
const BlockEmbed = Quill.import("blots/block/embed");
class TabsBlot extends BlockEmbed {
static blotName = "tabs";
static tagName = "div";
static className = "quill-tabs";
constructor(domNode) {
super(domNode);
this.bindEvents();
}
static create(value) {
const tabs = Array.isArray(value) ? value : [];
const node = super.create(value);
// 主容器样式
node.setAttribute(
"style",
`
margin: 15px 0;
overflow: hidden;
border: 1px solid #dddddd;
border-radius: 4px;
position: relative;
`
);
// 标签栏容器
const tabList = document.createElement("div");
tabList.className = "quill-tab-list";
tabList.style.cssText = `
display: flex;
background-color: #f8f9fa;
border-bottom: 1px solid #dddddd;
`;
// 内容区容器
const contentList = document.createElement("div");
contentList.className = "quill-tab-content-list";
contentList.style.cssText = "padding: 15px;";
// 生成标签和内容
tabs.forEach((tab, index) => {
// 标签按钮
const btn = document.createElement("button");
btn.className = `quill-tab-button ${index === 0 ? "active" : ""}`;
btn.dataset.index = index;
btn.textContent = tab.title || `标签${index + 1}`;
btn.style.cssText = `
padding: 10px 15px;
font-weight: 500;
cursor: pointer;
background: transparent;
border: none;
${index === 0 ? "color: #007bff; background-color: white; border-bottom: 2px solid #007bff;" : ""}
`;
tabList.appendChild(btn);
// 内容面板
const panel = document.createElement("div");
panel.className = `quill-tab-content ${index === 0 ? "active" : ""}`;
panel.dataset.index = index;
panel.innerHTML = tab.content || "";
panel.style.display = index === 0 ? "block" : "none";
contentList.appendChild(panel);
});
// 编辑按钮
const editBtn = document.createElement("button");
editBtn.className = "quill-tab-edit-btn";
editBtn.textContent = "编辑";
editBtn.dataset.action = "edit";
editBtn.style.cssText = `
display: flex;
align-items: center;
padding: 10px;
margin-left: auto;
color: #606266;
cursor: pointer;
background: transparent;
border: none;
`;
editBtn.onmouseover = () => (editBtn.style.color = "#007bff");
editBtn.onmouseout = () => (editBtn.style.color = "#606266");
tabList.appendChild(editBtn);
// 组装DOM
node.appendChild(tabList);
node.appendChild(contentList);
node.contentEditable = "false";
return node;
}
// 改进的事件绑定方法,避免重复绑定
bindEvents() {
// 使用WeakMap存储已绑定的元素避免重复绑定
if (!this.eventBoundElements) {
this.eventBoundElements = new WeakMap();
}
// 编辑按钮事件
const editBtn = this.domNode.querySelector(".quill-tab-edit-btn");
if (editBtn && !this.eventBoundElements.has(editBtn)) {
editBtn.addEventListener("click", e => {
e.stopPropagation();
this.domNode.dispatchEvent(
new CustomEvent("edit-tabs", {
bubbles: true,
detail: { blot: this }
})
);
});
this.eventBoundElements.set(editBtn, true);
}
// 标签切换事件
const tabButtons = this.domNode.querySelectorAll(".quill-tab-button:not([data-action])");
tabButtons.forEach(btn => {
if (!this.eventBoundElements.has(btn)) {
btn.addEventListener("click", () => {
const index = parseInt(btn.dataset.index, 10);
this.selectTab(index);
});
this.eventBoundElements.set(btn, true);
}
});
}
// 移除了有问题的unbindEvents方法
selectTab(index) {
const buttons = this.domNode.querySelectorAll(".quill-tab-button:not([data-action])");
const panels = this.domNode.querySelectorAll(".quill-tab-content");
if (index < 0 || index >= buttons.length) return;
// 更新按钮样式
buttons.forEach((btn, i) => {
btn.style.cssText = `
padding: 10px 15px;
font-weight: 500;
cursor: pointer;
background: transparent;
border: none;
${i === index ? "color: #007bff; background-color: white; border-bottom: 2px solid #007bff;" : ""}
`;
});
// 更新内容面板显示
panels.forEach((panel, i) => {
panel.style.display = i === index ? "block" : "none";
});
}
static value(node) {
const tabs = [];
const buttons = node.querySelectorAll(".quill-tab-button:not([data-action])");
const panels = node.querySelectorAll(".quill-tab-content");
buttons.forEach((btn, i) => {
tabs.push({
title: btn.textContent,
content: panels[i]?.innerHTML || ""
});
});
return tabs;
}
update(mutations, context) {
super.update(mutations, context);
this.bindEvents();
}
}
export default TabsBlot;

View File

@@ -1,24 +1,99 @@
import { Quill } from "@vueup/vue-quill";
// 源码中是import直接倒入这里要用Quill.import引入
const BlockEmbed = Quill.import("blots/block/embed");
const Link = Quill.import("formats/link");
const ATTRIBUTES = ["height", "width"];
const ATTRIBUTES = ["height", "width", "poster"];
class Video extends BlockEmbed {
static create(value) {
let node = super.create();
// 添加video标签所需的属性
// 基础视频属性
node.setAttribute("controls", "controls");
node.setAttribute("playsinline", "true");
node.setAttribute("webkit-playsinline", "true");
node.setAttribute("type", "video/mp4");
// poster 属性指定视频下载时显示的图像,或者在用户点击播放按钮前显示的图像。
node.setAttribute("poster", value.poster);
node.setAttribute("src", this.sanitize(value.url));
// 处理视频URL添加时间片段定位到0.001秒(避开黑屏)
const baseUrl = this.sanitize(value.url);
const videoUrl = baseUrl.includes("#") ? `${baseUrl}&t=0.001` : `${baseUrl}#t=0.001`;
node.setAttribute("src", videoUrl);
// 临时封面(加载中显示)
node.setAttribute(
"poster",
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='300' viewBox='0 0 600 300'%3E%3Crect width='100%25' height='100%25' fill='%23f0f0f0'/%3E%3Ccircle cx='300' cy='150' r='40' fill='%23ccc'/%3E%3Cpolygon points='300,130 330,160 270,160' fill='white'/%3E%3C/svg%3E"
);
// 自动截取封面未提供poster时
if (!value.poster) {
this.captureFrameAsBlob(videoUrl, node);
} else {
node.setAttribute("poster", this.sanitize(value.poster));
}
// width: 600px;
// height: 300px;
// object-fit: contain;
// 视频样式
node.setAttribute(
"style",
`
`
);
return node;
}
/**
* 生成视频帧Blob核心方法返回Promise
* @param {string} videoUrl - 视频的本地Blob URL
* @returns {Promise<Blob|null>} - 视频帧Blob或null
*/
/**
* 将视频Blob URL转换为JPG图片Blob
* @param {string} videoBlobUrl - 本地视频Blob URLblob:xxx
* @returns {Promise<Blob|null>} - JPG格式图片Blob失败返回null
*/
static async captureVideoFrame(videoBlobUrl) {
return new Promise(resolve => {
// 1. 创建视频元素加载Blob
const video = document.createElement("video");
video.src = videoBlobUrl;
video.muted = true;
video.playsInline = true;
video.preload = "auto";
// 2. 视频可播放时开始转换
video.oncanplay = () => {
// 3. 创建Canvas绘制视频帧
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth || 640; // 使用视频实际宽度
canvas.height = video.videoHeight || 360; // 使用视频实际高度
const ctx = canvas.getContext("2d");
// 绘制视频当前帧到Canvas
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 4. 将Canvas转换为JPG Blob
canvas.toBlob(
blob => {
console.log(blob, "================>");
video.remove(); // 清理视频元素
resolve(blob || null); // 返回JPG Blob
},
"image/jpeg", // 强制JPG格式
0.8 // 图片质量0-1
);
};
// 错误处理
video.onerror = () => {
video.remove();
resolve(null);
};
});
}
// 以下方法保持不变
static formats(domNode) {
return ATTRIBUTES.reduce((formats, attribute) => {
if (domNode.hasAttribute(attribute)) {
@@ -33,15 +108,14 @@ class Video extends BlockEmbed {
}
static value(domNode) {
// 设置自定义的属性值
return {
url: domNode.getAttribute("src"),
poster: domNode.getAttribute("poster")
url: domNode.getAttribute("src").split("#")[0],
poster: domNode.getAttribute("poster") || ""
};
}
format(name, value) {
if (ATTRIBUTES.indexOf(name) > -1) {
if (ATTRIBUTES.includes(name)) {
if (value) {
this.domNode.setAttribute(name, value);
} else {
@@ -53,12 +127,13 @@ class Video extends BlockEmbed {
}
html() {
const { video } = this.value();
return `<a href="${video}">${video}</a>`;
const { url, poster } = this.value();
return `<video src="${url}" ${poster ? `poster="${poster}"` : ""} controls playsinline webkit-playsinline ></video>`;
}
}
Video.blotName = "customVideo"; // 这里不用改不用iframe直接替换掉原来如果需要也可以保留原来的这里用个新的blot
Video.className = "ql-video"; // 可添加样式,看实际使用需要
Video.tagName = "video"; // 用video标签替换iframe
//style="width:600px;height:300px;"
Video.blotName = "customVideo";
// Video.className = "ql-video";
Video.tagName = "video";
export default Video;

View File

@@ -0,0 +1,72 @@
import { Quill } from "@vueup/vue-quill";
// 源码中是import直接倒入这里要用Quill.import引入
const BlockEmbed = Quill.import("blots/block/embed");
const Link = Quill.import("formats/link");
const ATTRIBUTES = ["height", "width"];
class Video extends BlockEmbed {
static create(value) {
let node = super.create();
// 添加video标签所需的属性
node.setAttribute("controls", "controls");
node.setAttribute("playsinline", "true");
node.setAttribute("webkit-playsinline", "true");
node.setAttribute("type", "video/mp4");
// poster 属性指定视频下载时显示的图像,或者在用户点击播放按钮前显示的图像。
// console.log(value.url, "= value.poster=");
// node.setAttribute("poster", this.sanitize(value.url));
node.setAttribute("src", this.sanitize(value.url));
node.setAttribute(
"style",
`
width: 600px;
height: 300px;
`
);
return node;
}
static formats(domNode) {
return ATTRIBUTES.reduce((formats, attribute) => {
if (domNode.hasAttribute(attribute)) {
formats[attribute] = domNode.getAttribute(attribute);
}
return formats;
}, {});
}
static sanitize(url) {
return Link.sanitize(url);
}
static value(domNode) {
// 设置自定义的属性值
return {
url: domNode.getAttribute("src")
// poster: domNode.getAttribute("src")
};
}
format(name, value) {
if (ATTRIBUTES.indexOf(name) > -1) {
if (value) {
this.domNode.setAttribute(name, value);
} else {
this.domNode.removeAttribute(name);
}
} else {
super.format(name, value);
}
}
html() {
const { video } = this.value();
return `<a href="${video}">${video}</a>`;
}
}
Video.blotName = "customVideo"; // 这里不用改不用iframe直接替换掉原来如果需要也可以保留原来的这里用个新的blot
Video.className = "ql-video"; // 可添加样式,看实际使用需要
Video.tagName = "video"; // 用video标签替换iframe
export default Video;

View File

@@ -52,13 +52,13 @@
class="m-2 select"
remote-show-suffix
:remote-method="
(query:any) => {
remoteMethod(
query,
item
);
}
"
(query:any) => {
remoteMethod(
query,
item
);
}
"
:disabled="item.disabled"
style="width: 224px"
>
@@ -77,9 +77,9 @@
:check-strictly="false"
show-checkbox
style="width: 224px"
clearable
:collapse-tags="true"
ref="treeSelectRef"
@change="handleTreeSelectChange(item)"
@remove-tag="handleRemoveTag(item, $event)"
/>
</template>
<!-- 双 -->
@@ -116,7 +116,7 @@
/>
</template>
<template v-if="item.type === 'selectInputs'">
<!-- <template v-if="item.type === 'selectInputs'">
<div></div>
<el-select
v-model="selectInputValue"
@@ -151,19 +151,19 @@
style="width: 106px !important"
@input="handleInput(item)"
/>
</template>
</template> -->
</div>
</template>
<script lang="ts" setup name="SearchFormItem">
// import { verificationInput } from "./utils/verificationInput";
import { getCountryListApi } from "@/api/modules/global";
import $Bus from "@/utils/mittBus";
import { cloneDeep } from "lodash-es";
import { ref } from "vue";
// import { ref } from "vue";
const $router = useRouter();
const routeName: any = ref($router.currentRoute.value.name);
const treeSelectRef = ref<any>(null);
// const userStore: any = useUserStore();
interface SearchFormItem {
item: { [key: string]: any };
@@ -171,17 +171,8 @@ interface SearchFormItem {
search: (params: any) => void; // 搜索方法
handleEmitClear?: (item: any) => void;
}
let selectInputValue = ref(1);
const options = [
{
value: 1,
label: "序号"
},
{
value: 2,
label: "数字序列号"
}
];
// let selectInputValue = ref();
// const treeSelectValue = ref(null);
const props = defineProps<SearchFormItem>();
const _searchParam = computed(() => props.searchParam);
@@ -220,7 +211,119 @@ const remoteMethod = async (query: any, item: any) => {
}
loading.value = false;
};
// 关闭标签时同步 - 修复版
const handleRemoveTag = (item: any, removedValue: any) => {
nextTick(() => {
// 从item.options获取树形数据这是我们传递给组件的数据源
const allNodes = Array.isArray(item.options) ? item.options : [];
console.log(allNodes, "=allNodes=");
// 找到被删除的节点
const removedNode = findNode(allNodes, removedValue);
console.log(removedNode, "=removedNode=");
let ids: any = [];
if (removedNode) {
// 判断被删除的是父节点还是子节点
if (removedNode.children && Array.isArray(removedNode.children) && removedNode.children.length > 0) {
// 是父节点,需要删除所有子节点
const childIds = getAllChildIds(removedNode);
// 从选中值中移除父节点和所有子节点
ids = (_searchParam.value[item.prop] || []).filter((id: any) => !childIds.includes(id) && id !== removedNode.id);
nextTick(() => {
_searchParam.value[item.prop] = ids;
_searchParam.value[item.prop1] = ids.join(",");
});
} else {
// 是子节点,需要找到其父节点并删除
const parentNode = findParentNode(allNodes, removedValue);
if (parentNode) {
// 从选中值中移除子节点和父节点
ids = (_searchParam.value[item.prop] || []).filter((id: any) => id !== removedValue && id !== parentNode.id);
nextTick(() => {
_searchParam.value[item.prop] = ids;
_searchParam.value[item.prop1] = ids.join(",");
});
} else {
ids = (_searchParam.value[item.prop] || []).filter((id: any) => id !== removedValue && id !== parentNode.id);
nextTick(() => {
_searchParam.value[item.prop] = ids;
_searchParam.value[item.prop1] = ids.join(",");
});
}
}
}
syncCheckedIds(item);
});
};
// 辅助方法:查找节点
const findNode = (nodes: any[], value: any): any => {
if (!Array.isArray(nodes)) return null;
for (const node of nodes) {
if (node?.id === value) {
return node;
}
if (node?.children && Array.isArray(node.children) && node.children.length > 0) {
const found = findNode(node.children, value);
if (found) return found;
}
}
return null;
};
// 辅助方法:查找父节点
const findParentNode = (nodes: any[], value: any, parent: any = null): any => {
if (!Array.isArray(nodes)) return null;
for (const node of nodes) {
if (node?.id === value) {
return parent;
}
if (node?.children && Array.isArray(node.children) && node.children.length > 0) {
const foundParent = findParentNode(node.children, value, node);
if (foundParent) return foundParent;
}
}
return null;
};
// 辅助方法获取所有子节点ID
const getAllChildIds = (node: any): any[] => {
let ids: any[] = [];
if (node?.children && Array.isArray(node.children) && node.children.length > 0) {
for (const child of node.children) {
if (child?.id) {
ids.push(child.id);
ids = [...ids, ...getAllChildIds(child)];
}
}
}
return ids;
};
// 统一同步选中ID的方法
const syncCheckedIds = (item: any) => {
// 获取所有全选中的节点(包括父节点)
const allCheckedNodes = treeSelectRef.value.getCheckedNodes(false, false);
const allCheckedIds = allCheckedNodes.map((node: any) => node.id);
_searchParam.value[item.prop] = allCheckedIds;
// 同步到搜索参数
_searchParam.value[item.prop1] = allCheckedIds.length ? allCheckedIds.join(",") : null;
};
const handleTreeSelectChange = (item: any) => {
console.log(routeName.value);
if (routeName.value === "articleListIndex") {
// 通过ref获取组件实例
if (treeSelectRef.value) {
syncCheckedIds(item);
return;
}
}
if (_searchParam.value[item.prop].length) {
let values = cloneDeep(_searchParam.value[item.prop]);
_searchParam.value[item.prop1] = values.join(",");
@@ -238,27 +341,17 @@ const handleInput = (item: any) => {
//验证
// verificationInput(item, _searchParam, selectInputValue.value);
};
const handleChange = (item: any) => {
_searchParam.value[item.endProp] = "";
_searchParam.value[item.startProp] = "";
_searchParam.value["serialNumberBegin"] = "";
_searchParam.value["numberCodeBegin"] = "";
_searchParam.value["serialNumberEnd"] = "";
_searchParam.value["numberCodeEnd"] = "";
};
// const handleChange = (item: any) => {
// _searchParam.value[item.endProp] = "";
// _searchParam.value[item.startProp] = "";
// _searchParam.value["serialNumberBegin"] = "";
// _searchParam.value["numberCodeBegin"] = "";
// _searchParam.value["serialNumberEnd"] = "";
// _searchParam.value["numberCodeEnd"] = "";
// };
const handleEmitClear = (item: any) => {
console.log(item);
if (routeName.value === "barCode") {
$Bus.emit("clearBarCodeCreateUser");
}
if (routeName.value === "boxCode") {
$Bus.emit("clearBoxCodeCreateUser");
}
if (routeName.value === "boxMarkIndex") {
$Bus.emit("clearBoxMarkIndexCreator");
}
};
</script>
<style lang="scss" scope>

View File

@@ -3,6 +3,8 @@
margin-bottom: 8px;
}
.form-item {
display: flex;
align-items: center;
width: 344px !important;
height: 32px;
margin-right: 12px !important;

View File

@@ -43,10 +43,9 @@ import { ref, computed, inject, watch } from "vue";
import { Plus, Delete } from "@element-plus/icons-vue";
import { uploadImg } from "@/api/modules/upload";
import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
import { generateUUID } from "@/utils";
// import { generateUUID } from "@/utils";
import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
let uid = generateUUID();
console.log("uid:", uid);
// let uid = generateUUID();
interface UploadFileProps {
fileList: UploadUserFile[];
api?: (params: any) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传

View File

@@ -53,7 +53,7 @@ const videoShowUrl = ref<any>(null);
const props = withDefaults(defineProps<UploadFileProps>(), {
videoUrl: "",
disabled: false,
fileSize: 200,
fileSize: 150,
width: "400px",
fileType: () => [".mp4", ".avi", ".mov", "video/mp4", "video/mov", "video/avi"],
borderRadius: "8px"

View File

@@ -46,7 +46,6 @@ const authStore = useAuthStore();
const globalStore = useGlobalStore();
const isCollapse = computed(() => globalStore.isCollapse);
const menuList = computed(() => authStore.showMenuListGet);
console.log(menuList, "=menuList=");
const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
</script>

View File

@@ -44,9 +44,16 @@ import { useMsg } from "@/hooks/useMsg";
import { useUserStore } from "@/stores/modules/user";
import { ElMessageBox } from "element-plus";
import { outLogin } from "@/utils/outLogin";
import { useKeepAliveStore } from "@/stores/modules/keepAlive";
import { getLanguageListApi, getLanguageCutoverApi } from "@/api/modules/global";
import { HOME_URL } from "@/config";
import { useRouter } from "vue-router";
import { useTabsStore } from "@/stores/modules/tabs";
const tabStore = useTabsStore();
const userStore = useUserStore();
const router = useRouter();
const keepAliveStore = useKeepAliveStore();
document.cookie = `lang=zh_cn`;
const langs = ref<any>([]);
const name = ref("");
@@ -58,13 +65,13 @@ const getLanguageList = async () => {
const { data } = result;
langs.value = data;
let id = userStore?.languageType ? userStore?.languageType : data[0]?.id;
getLanguageCutover(id);
getLanguageCutover(id, "noCLick");
}
};
getLanguageList();
// 站点切换接口
const getLanguageCutover = async (id: any) => {
const getLanguageCutover = async (id: any, type: any) => {
const result = await getLanguageCutoverApi(id);
if (result?.code === 0) {
userStore.setLanguageType(id);
@@ -72,12 +79,35 @@ const getLanguageCutover = async (id: any) => {
return item.id === id;
});
name.value = names[0]?.country_name;
if (type === "click") {
tabStore.setTabs([
{
icon: "",
title: "首页",
path: "/admin/index",
name: "home",
close: true
}
]);
keepAliveStore.setKeepAliveName();
setTimeout(() => {
router.push(HOME_URL);
}, 500);
}
}
};
// 站点切换事件
const handleCommand = (val: string) => {
getLanguageCutover(val);
//切换语言将清空所有标签页面
ElMessageBox.confirm("切换语言将清空所有标签页面?", "温馨提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await getLanguageCutover(val, "click");
});
};
// 退出登录

View File

@@ -40,6 +40,7 @@ watch(
);
const handleOpenPage = (item: any) => {
console.log(item, "===========item==========");
$router.push({
path: item.path
});

View File

@@ -86,9 +86,23 @@ const closeOtherTab = () => {
// Close All
const closeAllTab = () => {
tabStore.closeMultipleTab();
tabStore.setTabs([
{
icon: "",
title: "首页",
path: "/admin/index",
name: "home",
close: true
}
]);
keepAliveStore.setKeepAliveName();
router.push(HOME_URL);
setTimeout(() => {
router.push(HOME_URL);
}, 500);
// tabStore.closeMultipleTab();
// keepAliveStore.setKeepAliveName();
// router.push(HOME_URL);
};
</script>

View File

@@ -36,6 +36,7 @@ import MoreButton from "./components/MoreButton.vue";
const route = useRoute();
const router = useRouter();
const tabStore = useTabsStore();
const authStore = useAuthStore();
// const globalStore = useGlobalStore();
@@ -44,7 +45,7 @@ const keepAliveStore = useKeepAliveStore();
const tabsMenuValue = ref(route.fullPath);
const tabsMenuList = computed(() => tabStore.tabsMenuList);
console.log(tabStore.tabsMenuList, "===============value================");
onMounted(() => {
tabsDrop();
initTabs();

View File

@@ -30,12 +30,13 @@ export const useTabsStore = defineStore({
},
// Close MultipleTab
async closeMultipleTab(tabsMenuValue?: string) {
this.tabsMenuList = this.tabsMenuList.filter(item => {
return item.path === tabsMenuValue || !item.close;
this.tabsMenuList = this.tabsMenuList.filter((item: any) => {
return item.path === tabsMenuValue || !item.hidden;
});
},
// Set Tabs
async setTabs(tabsMenuList: any[]) {
console.log(tabsMenuList, "=tabsMenuList=");
this.tabsMenuList = tabsMenuList;
},
// Set Tabs Title

View File

@@ -27,6 +27,12 @@ export const EDIT_FORM_DATA: FormItem[] = [
type: "input",
label: "Banner名称: "
},
{
prop: "short_title",
placeholder: "请输入",
type: "input",
label: "Banner简称: "
},
{
prop: "title_txt_color",
placeholder: "填写RGB值",
@@ -127,6 +133,12 @@ export const EDIT_FORM_DATA1: FormItem[] = [
type: "input",
label: "Banner名称: "
},
{
prop: "short_title",
placeholder: "请输入",
type: "input",
label: "Banner简称: "
},
{
prop: "title_txt_color",
placeholder: "填写RGB值",

View File

@@ -356,16 +356,18 @@ const handleAdd = () => {
dataStore.visible = true;
selectedNodes.value = "";
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
(dataStore.editFormData = cloneDeep(EDIT_FORM_DATA)), // 抽屉表单配置项
getBannerClassEditList();
dataStore.editFormData = cloneDeep(EDIT_FORM_DATA); // 抽屉表单配置项
dataStore.rules = cloneDeep(RULES);
getBannerClassEditList();
// getBannerClassList();
getProductCategoryList();
};
// 抽屉关闭前的钩子
const handleBeforeClone = () => {
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
(dataStore.editFormData = cloneDeep(EDIT_FORM_DATA)), // 抽屉表单配置项
resetFields();
dataStore.editFormData = cloneDeep(EDIT_FORM_DATA); // 抽屉表单配置项
dataStore.rules = cloneDeep(RULES);
resetFields();
dataStore.visible = false;
dataStore.isFirstRequest = true;
};

View File

@@ -3,11 +3,23 @@
<!-- 封面图 -->
<div>
<h5 style="margin: 0; margin-bottom: 16px; font-size: 14px">封面图</h5>
<UploadImg v-model:image-url="imgInfoDataStore.cover_image">
<template #tip>
<div style="width: 150px; text-align: center">图片尺寸800x800</div>
</template>
</UploadImg>
<div style="display: flex">
<div>
<UploadImg v-model:image-url="imgInfoDataStore.cover_image">
<template #tip>
<div style="width: 150px; text-align: center">图片尺寸800x800</div>
</template>
</UploadImg>
</div>
<!-- <div style="margin-left: 20px">
<UploadImg v-model:image-url="imgInfoDataStore.cover_image">
<template #tip>
<div style="width: 150px; text-align: center">图片尺寸800x800</div>
</template>
</UploadImg>
</div> -->
</div>
</div>
<el-divider />
<!-- 属性 -->
@@ -116,7 +128,6 @@ const findAttrById = (id: any) => {
// 在 row 的 attrs 数组中查找对应 attrId 的对象
const findAttrObjInRow = (row: any, attrId: any) => {
console.log(row.attrs, "=======row===========");
let obj = row.attrs.find((item: any) => item.attr_id === attrId.toString());
if (!obj) {
obj = { attr_id: attrId.toString(), attr_value: "" };
@@ -190,7 +201,6 @@ const echoData = () => {
});
imgInfoDataStore.skus = newSkus;
console.log(imgInfoDataStore, "=imgInfoDataStore=");
handleCheckboxChange();
};
const callEchoDataIfHasValue = () => {

View File

@@ -35,27 +35,10 @@ export const FORM_DATA: FormItem[] = [
prop: "treeIds",
prop1: "category_id",
placeholder: "请选择",
type: "treeSelect",
type: "treeSelect", //treeSelect
isArray: true,
label: "产品分类: ",
options: [
{
value: "1",
label: "Level one 1",
children: [
{
value: "1-1",
label: "Level two 1-1",
children: [
{
value: "1-1-1",
label: "Level three 1-1-1"
}
]
}
]
}
]
options: []
},
{
prop: "Time",

View File

@@ -15,6 +15,35 @@ const hasIdRecursive = (data: any, targetId: any) => {
return false;
};
const htmlDecode = (html: any) => {
let e: any = document.createElement("div");
e.innerHTML = html;
// 关键:在解码后添加样式处理
const detailAllElements = e.querySelectorAll(".o_detail_all");
detailAllElements.forEach((detailAll: HTMLElement) => {
// 为文字类子元素添加居中样式
let textElements: any = [
...Array.from(detailAll.querySelectorAll<HTMLElement>(".o_detail_text")),
...Array.from(detailAll.querySelectorAll<HTMLElement>(".o_detail_small")),
...Array.from(detailAll.querySelectorAll<HTMLElement>(".o_detail_title"))
];
textElements.forEach((el: any) => {
// 保留原有样式,追加居中样式(避免覆盖已有样式)
el.style.textAlign = "center";
// 如果需要强制覆盖,可添加 !important
// el.style.textAlign = 'center !important';
});
});
if (e.childNodes.length > 1) {
return e.innerHTML;
} else {
return e.childNodes[0].innerHTML;
}
};
//将参数分离
export const initDetailParams = (dataStore: any, data: any, editorRef: any) => {
let is = hasIdRecursive(dataStore.options, data.category_id);
@@ -40,15 +69,13 @@ export const initDetailParams = (dataStore: any, data: any, editorRef: any) => {
stock_qty: data.stock_qty,
id: data.id
});
console.log(data.detail, "=======detail========");
//详情
if (!data.detail) {
dataStore.detail = "";
editorRef?.value?.clearEditor(); // 调用子组件的清空方法
} else {
dataStore.detail = data.detail;
// console.log(dataStore.detail, "=dataStore.detail=");
dataStore.detail = htmlDecode(data.detail); //htmlDecode(data.detail);
}
//图片

View File

@@ -50,6 +50,7 @@
highlight-current
:props="defaultProps"
@check-change="handleTreeCheckChange"
:check-strictly="false"
/>
</div>
</div>
@@ -77,49 +78,54 @@ import {
getRoleListEditUpApi,
getRoleListSaveApi
} from "@/api/modules/roleList";
//getMenusListApi
//权限列表接口
import { getRoleMenusListApi } from "@/api/modules/webMenusList";
//深拷贝方法
import { cloneDeep } from "lodash-es";
//表格和搜索
//表格和搜索
import { RULE_FORM, FORM_DATA, COLUMNS, EDIT_FORM_DATA, EDIT_RULE_FORM, RULES } from "./constant/index";
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
// 组件引用
const proTableRef = ref<any>(null);
const formRef: any = ref(null);
const treeRef: any = ref(null);
const defaultProps = {
children: "children",
label: "title"
label: "title",
disabled: (data: any) => data.id === 7 // 首页节点ID为7禁止选择
};
// 数据源
const dataStore = reactive<any>({
treeData: [], //权限
allCheck: false,
treeData: [], //权限树数据
allCheck: false, //全选框状态
title: "添加角色", //抽屉标题
columns: COLUMNS, //列表配置项
rules: cloneDeep(RULES), //抽屉表单验证
editRuleForm: cloneDeep(EDIT_RULE_FORM),
editFormData: cloneDeep(EDIT_FORM_DATA), //抽屉表单配置项
initParam: cloneDeep(RULE_FORM), // 初始化搜索条件|重置搜索条件
ruleForm: cloneDeep(RULE_FORM), // 搜索參數
rules: cloneDeep(RULES), //表单验证规则
editRuleForm: cloneDeep(EDIT_RULE_FORM), //编辑表单数据
editFormData: cloneDeep(EDIT_FORM_DATA), //表单配置项
initParam: cloneDeep(RULE_FORM), //初始化搜索条件
ruleForm: cloneDeep(RULE_FORM), //搜索参数
formData: FORM_DATA, //搜索配置项
selectedMenuIds: [], // 新增属性,用于保存选中的菜单 id
isIndeterminate: false, //全选框样式控制
visible: false, //抽屉控制
selectRow: {} //当前选择的row
selectedMenuIds: [], //选中的菜单ID含全选和半选
isIndeterminate: false, //全选框半选样式
visible: false, //抽屉显示状态
selectRow: {} //当前选中行数据
});
// 处理全选框状态变化
// 处理全选框状态变化(排除首页节点)
const handleAllCheckChange = (checked: any) => {
const allNodeKeys = getAllNodeKeys(dataStore.treeData);
const allNodeKeys = getAllNodeKeys(dataStore.treeData).filter(id => id !== 7); // 排除首页ID
if (checked) {
treeRef.value.setCheckedKeys(allNodeKeys);
// 全选时选中所有节点(包括父节点,但首页已禁用)
treeRef.value.setCheckedKeys([7, ...allNodeKeys]); // 强制包含首页
} else {
treeRef.value.setCheckedKeys([]);
// 取消全选时保留首页选中状态
treeRef.value.setCheckedKeys([7]);
}
};
// 获取所有节点的 key
// 获取所有节点的ID用于全选计算
const getAllNodeKeys = (data: any[]) => {
let keys: number[] = [];
data.forEach(item => {
@@ -131,139 +137,187 @@ const getAllNodeKeys = (data: any[]) => {
return keys;
};
// 处理树节点选中状态变化
// 处理树节点选中状态变化(确保首页始终选中)
const handleTreeCheckChange = () => {
const allNodeKeys = getAllNodeKeys(dataStore.treeData);
// 移除 (data: any) 参数
// 防止手动取消首页选中(虽然已禁用,但做双重保险)
if (!dataStore.selectedMenuIds.some((item: any) => item.menu_id === 7)) {
dataStore.selectedMenuIds.unshift({ menu_id: 7 });
}
const allNodeKeys = getAllNodeKeys(dataStore.treeData).filter(id => id !== 7); // 排除首页
// 获取全选节点ID包括父节点全选和叶子节点选中
const checkedKeys = treeRef.value.getCheckedKeys();
if (checkedKeys.length === allNodeKeys.length) {
// 获取半选节点ID父节点部分子节点选中
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys();
// 合并全选和半选ID并去重强制包含首页
const allSelectedKeys = [...new Set([7, ...checkedKeys, ...halfCheckedKeys])];
// 更新全选框状态(排除首页计算)
const selectedWithoutHome = allSelectedKeys.filter(id => id !== 7);
if (selectedWithoutHome.length === allNodeKeys.length) {
dataStore.allCheck = true;
dataStore.isIndeterminate = false;
} else if (checkedKeys.length === 0) {
} else if (selectedWithoutHome.length === 0) {
dataStore.allCheck = false;
dataStore.isIndeterminate = false;
} else {
dataStore.allCheck = false;
dataStore.isIndeterminate = true;
}
// 更新选中的菜单 id
dataStore.selectedMenuIds = checkedKeys.map((id: any) => ({ menu_id: id }));
// 保存所有选中的菜单ID强制包含首页
dataStore.selectedMenuIds = allSelectedKeys.map((id: number) => ({ menu_id: id }));
};
//抽屉确认
// 抽屉确认按钮
const handleConfirmClick = () => {
if (!formRef.value!.ruleFormRef) return;
formRef!.value!.ruleFormRef!.validate((valid: any) => {
if (!formRef.value?.ruleFormRef) return;
formRef.value.ruleFormRef.validate((valid: boolean) => {
if (valid) {
console.log("submit!");
dataStore.title === "添加角色" ? getRoleListSave() : getRoleListEditUp();
} else {
console.log("error submit!");
console.log("表单验证失败");
return false;
}
});
};
//重置验证状态
// 重置表单验证状态
const resetFields = () => {
if (!formRef.value!.ruleFormRef) return;
formRef!.value!.ruleFormRef.resetFields();
if (formRef.value?.ruleFormRef) {
formRef.value.ruleFormRef.resetFields();
}
};
//抽屉重置
// 抽屉重置按钮
const handleResetClick = () => {
if (dataStore.title === "添加角色") {
resetFields();
// 重置树选择状态(保留首页选中)
treeRef.value?.setCheckedKeys([7]);
dataStore.selectedMenuIds = [{ menu_id: 7 }];
} else {
// 编辑时重新获取详情数据
getRoleListDetails(dataStore.editRuleForm.id);
}
};
//添加
// 打开添加抽屉
const handleAdd = () => {
dataStore.title = "添加角色";
dataStore.visible = true;
// 重置表单和树状态(默认选中首页)
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
resetFields();
nextTick(() => {
treeRef.value?.setCheckedKeys([7]);
});
};
//抽屉关闭前的钩子
// 抽屉关闭前处理
const handleBeforeClone = () => {
dataStore.selectedMenuIds = [];
treeRef.value.setCheckedKeys([]);
treeRef.value?.setCheckedKeys([]);
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
resetFields();
dataStore.visible = false;
};
//按钮点击事件
const handleBtnClick = (type: any, row: any) => {
// 编辑/删除按钮点击
const handleBtnClick = (type: string, row: any) => {
dataStore.selectRow = row;
//编辑
if (type === "编辑") {
dataStore.visible = true;
dataStore.title = "编辑角色";
getRoleListDetails(row.id);
return;
}
//删除
if (type === "删除") {
} else if (type === "删除") {
getRoleListDel(row.id);
}
};
//删除
const getRoleListDel = (id: any) => {
// 删除角色
const getRoleListDel = (id: number) => {
messageBox("你确定要删除?", async () => {
const result = await getRoleListDelApi(id);
if (result?.code === 0) {
const { msg } = result;
useMsg("success", msg);
proTableRef?.value?.getTableList();
useMsg("success", result.msg);
proTableRef.value?.getTableList();
}
});
};
//详情
const getRoleListDetails = async (id: any) => {
// 获取角色详情并回显(确保首页始终选中)
const getRoleListDetails = async (id: number) => {
const result = await getRoleListDetailsApi(id);
if (result?.code === 0) {
dataStore.editRuleForm = result?.data;
const menuIds = dataStore.editRuleForm.authorities.map((item: any) => item.menu_id);
// 设置树组件的默认选中节点
dataStore.editRuleForm = result.data;
// 提取后台返回的权限ID列表强制包含首页
let savedMenuIds = dataStore.editRuleForm.authorities.map((item: any) => item.menu_id);
savedMenuIds = [...new Set([7, ...savedMenuIds])]; // 确保首页ID存在
nextTick(() => {
if (treeRef.value) {
treeRef.value.setCheckedKeys(menuIds);
// 筛选出所有叶子节点ID用于正确触发父节点半选状态
const leafIds: number[] = [];
const findLeafNodes = (nodes: any[]) => {
nodes.forEach(node => {
if (!node.children || node.children.length === 0) {
// 叶子节点且在保存的ID中才选中
if (savedMenuIds.includes(node.id)) {
leafIds.push(node.id);
}
} else {
findLeafNodes(node.children);
}
});
};
findLeafNodes(dataStore.treeData);
// 设置叶子节点选中状态,强制包含首页
treeRef.value.setCheckedKeys([7, ...leafIds]);
// 触发状态更新
handleTreeCheckChange();
}
});
}
};
//保存
// 保存新角色
const getRoleListSave = async () => {
const result = await getRoleListSaveApi({
...dataStore.editRuleForm,
menu_permission: JSON.stringify(dataStore.selectedMenuIds)
});
if (result?.code === 0) {
const { msg } = result;
useMsg("success", msg);
useMsg("success", result.msg);
dataStore.visible = false;
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
proTableRef?.value?.getTableList();
proTableRef.value?.getTableList();
}
};
//更新
// 更新角色信息
const getRoleListEditUp = async () => {
const result = await getRoleListEditUpApi({
...dataStore.editRuleForm,
menu_permission: JSON.stringify(dataStore.selectedMenuIds)
});
if (result?.code === 0) {
const { msg } = result;
useMsg("success", msg);
useMsg("success", result.msg);
dataStore.visible = false;
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
proTableRef?.value?.getTableList();
proTableRef.value?.getTableList();
}
};
//权限管理
// 获取权限树数据
const getMenusList = async () => {
const result = await getRoleMenusListApi();
if (result?.code === 0) {
dataStore.treeData = result?.data;
dataStore.treeData = result.data;
}
};
// 初始化加载权限树
getMenusList();
</script>

View File

@@ -0,0 +1,316 @@
<template>
<div class="table-box">
<div style="padding-bottom: 16px">
<el-button type="primary" @click="handleAdd"> 添加 </el-button>
</div>
<ProTable
ref="proTableRef"
:formData="dataStore.formData"
:columns="dataStore.columns"
:request-api="getRoleListApi"
:init-param="dataStore.initParam"
>
<template #operation="scope">
<el-button size="small" type="primary" @click="handleBtnClick('编辑', scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleBtnClick('删除', scope.row)">删除</el-button>
</template>
</ProTable>
<el-drawer
v-model="dataStore.visible"
:show-close="true"
:size="600"
:close-on-click-modal="false"
:close-on-press-escape="false"
:before-close="handleBeforeClone"
destroy-on-close
>
<template #header="{ titleId, titleClass }">
<h4 :id="titleId" :class="titleClass">{{ dataStore.title }}</h4>
</template>
<div>
<rulesForm
:ruleForm="dataStore.editRuleForm"
:formData="dataStore.editFormData"
:rules="dataStore.rules"
:indeterminate="dataStore.isIndeterminate"
ref="formRef"
>
</rulesForm>
<div style="margin-left: 65px; font-size: 14px; color: #606266">
<div style="display: flex; align-items: center; margin-bottom: 10px">
<span style="margin-right: 10px">权限分配:</span>
<el-checkbox v-model="dataStore.allCheck" @change="handleAllCheckChange" label="全选" size="large" />
<!-- <el-checkbox v-model="dataStore.allCheck" @change="handleAllCheckChange" label="全选" size="large" /> -->
</div>
<el-tree
ref="treeRef"
style="max-width: 600px"
:data="dataStore.treeData"
show-checkbox
node-key="id"
highlight-current
:props="defaultProps"
@check-change="handleTreeCheckChange"
/>
</div>
</div>
<template #footer>
<div style="flex: auto">
<el-button @click="handleResetClick">重置</el-button>
<el-button type="primary" @click="handleConfirmClick">确认</el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="roleListIndex">
import ProTable from "@/components/ProTable/index.vue";
import rulesForm from "@/components/rulesForm/index.vue";
import { messageBox } from "@/utils/messageBox";
import { nextTick } from "vue";
import { useMsg } from "@/hooks/useMsg";
//列表接口
import {
getRoleListApi,
getRoleListDetailsApi,
getRoleListDelApi,
getRoleListEditUpApi,
getRoleListSaveApi
} from "@/api/modules/roleList";
//权限列表接口
import { getRoleMenusListApi } from "@/api/modules/webMenusList";
//深拷贝方法
import { cloneDeep } from "lodash-es";
//表格和搜索条件
import { RULE_FORM, FORM_DATA, COLUMNS, EDIT_FORM_DATA, EDIT_RULE_FORM, RULES } from "./constant/index";
// 组件引用
const proTableRef = ref<any>(null);
const formRef: any = ref(null);
const treeRef: any = ref(null);
const defaultProps = {
children: "children",
label: "title"
};
// 数据源
const dataStore = reactive<any>({
treeData: [], //权限树数据
allCheck: false, //全选框状态
title: "添加角色", //抽屉标题
columns: COLUMNS, //列表配置项
rules: cloneDeep(RULES), //表单验证规则
editRuleForm: cloneDeep(EDIT_RULE_FORM), //编辑表单数据
editFormData: cloneDeep(EDIT_FORM_DATA), //表单配置项
initParam: cloneDeep(RULE_FORM), //初始化搜索条件
ruleForm: cloneDeep(RULE_FORM), //搜索参数
formData: FORM_DATA, //搜索配置项
selectedMenuIds: [], //选中的菜单ID含全选和半选
isIndeterminate: false, //全选框半选样式
visible: false, //抽屉显示状态
selectRow: {} //当前选中行数据
});
// 处理全选框状态变化
const handleAllCheckChange = (checked: any) => {
const allNodeKeys = getAllNodeKeys(dataStore.treeData);
if (checked) {
// 全选时选中所有节点(包括父节点)
treeRef.value.setCheckedKeys(allNodeKeys);
} else {
// 取消全选时清空所有选中
treeRef.value.setCheckedKeys([]);
}
};
// 获取所有节点的ID
const getAllNodeKeys = (data: any[]) => {
let keys: number[] = [];
data.forEach(item => {
keys.push(item.id);
if (item.children && item.children.length > 0) {
keys = keys.concat(getAllNodeKeys(item.children));
}
});
return keys;
};
// 处理树节点选中状态变化
const handleTreeCheckChange = () => {
const allNodeKeys = getAllNodeKeys(dataStore.treeData);
// 获取全选节点ID包括父节点全选和叶子节点选中
const checkedKeys = treeRef.value.getCheckedKeys();
// 获取半选节点ID父节点部分子节点选中
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys();
// 合并全选和半选ID并去重
const allSelectedKeys = [...new Set([...checkedKeys, ...halfCheckedKeys])];
// 更新全选框状态
if (allSelectedKeys.length === allNodeKeys.length) {
dataStore.allCheck = true;
dataStore.isIndeterminate = false;
} else if (allSelectedKeys.length === 0) {
dataStore.allCheck = false;
dataStore.isIndeterminate = false;
} else {
dataStore.allCheck = false;
dataStore.isIndeterminate = true;
}
// 保存所有选中的菜单ID含半选父节点
dataStore.selectedMenuIds = allSelectedKeys.map((id: number) => ({ menu_id: id }));
};
// 抽屉确认按钮
const handleConfirmClick = () => {
if (!formRef.value?.ruleFormRef) return;
formRef.value.ruleFormRef.validate((valid: boolean) => {
if (valid) {
dataStore.title === "添加角色" ? getRoleListSave() : getRoleListEditUp();
} else {
console.log("表单验证失败");
return false;
}
});
};
// 重置表单验证状态
const resetFields = () => {
if (formRef.value?.ruleFormRef) {
formRef.value.ruleFormRef.resetFields();
}
};
// 抽屉重置按钮
const handleResetClick = () => {
if (dataStore.title === "添加角色") {
resetFields();
// 重置树选择状态
treeRef.value?.setCheckedKeys([]);
dataStore.selectedMenuIds = [];
} else {
// 编辑时重新获取详情数据
getRoleListDetails(dataStore.editRuleForm.id);
}
};
// 打开添加抽屉
const handleAdd = () => {
dataStore.title = "添加角色";
dataStore.visible = true;
// 重置表单和树状态
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
resetFields();
nextTick(() => {
treeRef.value?.setCheckedKeys([]);
});
};
// 抽屉关闭前处理
const handleBeforeClone = () => {
dataStore.selectedMenuIds = [];
treeRef.value?.setCheckedKeys([]);
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
resetFields();
dataStore.visible = false;
};
// 编辑/删除按钮点击
const handleBtnClick = (type: string, row: any) => {
dataStore.selectRow = row;
if (type === "编辑") {
dataStore.visible = true;
dataStore.title = "编辑角色";
getRoleListDetails(row.id);
} else if (type === "删除") {
getRoleListDel(row.id);
}
};
// 删除角色
const getRoleListDel = (id: number) => {
messageBox("你确定要删除?", async () => {
const result = await getRoleListDelApi(id);
if (result?.code === 0) {
useMsg("success", result.msg);
proTableRef.value?.getTableList();
}
});
};
// 获取角色详情并回显
const getRoleListDetails = async (id: number) => {
const result = await getRoleListDetailsApi(id);
if (result?.code === 0) {
dataStore.editRuleForm = result.data;
// 提取后台返回的权限ID列表
const savedMenuIds = dataStore.editRuleForm.authorities.map((item: any) => item.menu_id);
nextTick(() => {
if (treeRef.value) {
// 筛选出所有叶子节点ID用于正确触发父节点半选状态
const leafIds: number[] = [];
const findLeafNodes = (nodes: any[]) => {
nodes.forEach(node => {
if (!node.children || node.children.length === 0) {
// 叶子节点且在保存的ID中才选中
if (savedMenuIds.includes(node.id)) {
leafIds.push(node.id);
}
} else {
findLeafNodes(node.children);
}
});
};
findLeafNodes(dataStore.treeData);
// 设置叶子节点选中状态,父节点会自动计算半选/全选
treeRef.value.setCheckedKeys(leafIds);
// 触发状态更新
handleTreeCheckChange();
}
});
}
};
// 保存新角色
const getRoleListSave = async () => {
const result = await getRoleListSaveApi({
...dataStore.editRuleForm,
menu_permission: JSON.stringify(dataStore.selectedMenuIds)
});
if (result?.code === 0) {
useMsg("success", result.msg);
dataStore.visible = false;
proTableRef.value?.getTableList();
}
};
// 更新角色信息
const getRoleListEditUp = async () => {
const result = await getRoleListEditUpApi({
...dataStore.editRuleForm,
menu_permission: JSON.stringify(dataStore.selectedMenuIds)
});
if (result?.code === 0) {
useMsg("success", result.msg);
dataStore.visible = false;
proTableRef.value?.getTableList();
}
};
// 获取权限树数据
const getMenusList = async () => {
const result = await getRoleMenusListApi();
if (result?.code === 0) {
dataStore.treeData = result.data;
console.log(result.data, "==============data===============");
}
};
// 初始化加载权限树
getMenusList();
</script>
<style scoped></style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long