12 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
23 changed files with 2018 additions and 404 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

@@ -29,8 +29,8 @@
<QuillEditor
id="mainEditor"
ref="myQuillEditor"
contentType="html"
v-model:content="editorContent"
contentType="html"
@update:content="onContentChange"
:options="options"
/>
@@ -53,9 +53,7 @@
editable
@edit="handleTabsEdit"
@tab-change="handleTabChange"
v-if="tabsData.length"
>
<!-- 标签页标题支持编辑 -->
<el-tab-pane
:label="item.title"
:name="item.key"
@@ -66,11 +64,9 @@
>
<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
@@ -86,7 +82,6 @@
</div>
</template>
<!-- 标签页编辑器内容 -->
<QuillEditor
:id="`tabEditor_${item.key}`"
:ref="
@@ -108,33 +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";
// /computed
import { getCurrentInstance, reactive, ref, toRaw, onMounted, nextTick } from "vue";
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;
// 自定义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();
@@ -147,35 +202,40 @@ 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 = reactive([]);
// 标题编辑输入框的ref
const tabEditors = 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: () => {
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",
@@ -198,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);
},
@@ -217,10 +278,11 @@ const options = reactive({
readOnly: props.readOnly
});
// 标签页编辑器配置(保持不变)
// 标签页编辑器配置
const options1 = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -240,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);
},
@@ -257,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);
@@ -291,171 +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[tabIndex]);
quill = rawQuillEditor.getQuill();
}
uploadingCount.value--;
imageListDb.value.forEach(item => {
const length = quill.getLength() - 1;
quill.insertEmbed(length, "customImage", {
url: item.path,
id: item.serverImgId || generateUUID()
// 所有图片上传完成后显示排序弹窗
if (uploadingCount.value === 0) {
sortedImageList.value = [...imageListDb.value];
updateSortOrder(); // 确保排序索引正确
nextTick(() => {
showImageSortDialog.value = true;
});
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 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 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 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 file = evt.target.files[0];
// 1. 校验视频文件
const maxSize = props.fileSizeLimit * 1024 * 1024 * 15;
// 校验视频文件
const maxSize = 150 * 1024 * 1024;
if (file.size > maxSize) {
ElNotification({
title: "文件过大",
message: `视频大小不能超过 ${props.fileSizeLimit}MB`,
message: `视频大小不能超过 ${150}MB`,
type: "warning"
});
evt.target.value = "";
return;
}
// 2. 生成视频本地URL(用于生成封面,不上传)
// 生成视频本地URL
const localVideoUrl = URL.createObjectURL(file);
try {
// 4. 并行处理:上传视频 + 生成并上传封面
// 4.1 上传视频到视频服务器
// 上传视频
const videoFormData = new FormData();
videoFormData.append("video", file);
const videoRes = await uploadVideo(videoFormData); // 视频上传接口
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 videoUrl = videoRes.data.path;
// 复用图片上传接口(与图片上传逻辑一致)
// 生成封面图并上传
const frameBlob = await Video.captureVideoFrame(localVideoUrl);
let coverUrl = "";
if (frameBlob) {
const coverFormData = new FormData();
const coverUid = generateUUID(); // 生成唯一ID
// formData.append("image", options.file);
const coverUid = generateUUID();
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("封面上传失败,使用默认封面");
coverUrl = coverRes.data.data.path;
}
}
// 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 = ""; // 重置文件输入框
URL.revokeObjectURL(localVideoUrl);
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[tabIndex])?.getQuill();
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
poster: coverUrl
});
quill.setSelection(range.index + 1); // 移动光标到视频后
quill.setSelection(range.index + 1);
}
};
// 标签页切换事件基于key切换
// 标签页切换事件
const handleTabChange = key => {
const tabIndex = tabsData.value.findIndex(item => item.key === key);
activeName.value = key;
@@ -468,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.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;
}
@@ -510,49 +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 => {
console.log(content, "=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;
@@ -561,26 +648,16 @@ const handleQR = () => {
}
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.insertText(range.index, "\n"); // 插入换行
quill.setSelection(range.index + 1);
quill.insertText(range.index, "\n"); // 插入换行
quill.insertText(range.index, "\n");
}
// 关闭弹窗并清空临时数据
setTabsInfo();
};
//取消
const handleQX = () => {
setTabsInfo();
};
@@ -592,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;
@@ -615,26 +688,19 @@ const loadTabsDataToEditor = tabs => {
};
onMounted(() => {
nextTick(() => {
initTitle();
// 监听编辑按钮点击事件
const editorEl = document.querySelector(".ql-editor");
if (editorEl) {
editorEl.addEventListener("edit-tabs", e => {
console.log(e.detail.blot, "=e.detail=");
const tabsData = TabsBlot.value(e.detail.blot.domNode);
if (tabsData.length > 0) {
// 保存当前编辑的标签页引用
currentEditingTabsRef.value = e.detail.blot;
// 加载数据到弹窗
loadTabsDataToEditor(tabsData);
// 显示弹窗
outerVisible.value = true;
}
});
}
});
});
defineExpose({
clearEditor: () => {
@@ -650,28 +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>
./quill-image1111

View File

@@ -123,17 +123,35 @@ 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"]);
@@ -159,12 +177,15 @@ const currentEditingTabsRef = ref(null);
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);
}
@@ -175,6 +196,7 @@ const myQuillEditor = ref(null); // 主编辑器ref
const options = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -208,6 +230,7 @@ const options = reactive({
}
}
},
placeholder: "请输入内容...",
readOnly: props.readOnly
});
@@ -216,6 +239,7 @@ const options = reactive({
const options1 = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -256,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)) {
@@ -271,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);
@@ -287,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 = "";
@@ -304,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: 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 = [];
@@ -328,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: 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);
@@ -514,20 +776,49 @@ const loadTabsDataToEditor = tabs => {
});
};
// 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 => {
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);
// 显示弹窗
@@ -535,8 +826,15 @@ onMounted(() => {
}
});
}
// 等待编辑器首次渲染完成
// setTimeout(handleCleanEmptyTags, 300);
});
// 监听内容变化,重新清理空标签
// watch(editorContent, () => {
// nextTick(() => {
// setTimeout(handleCleanEmptyTags, 100); // 延迟确保 Quill 已重新渲染
// });
// });
defineExpose({
clearEditor: () => {
const quill = toRaw(myQuillEditor.value)?.getQuill();
@@ -574,5 +872,89 @@ defineExpose({
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>
./quill-image1111

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

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

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

View File

@@ -3,12 +3,24 @@
<!-- 封面图 -->
<div>
<h5 style="margin: 0; margin-bottom: 16px; font-size: 14px">封面图</h5>
<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 />
<!-- 属性 -->
<div>
@@ -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: "" };

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,14 +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;
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