26 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
b3bffac35e feat: 🚀 富文本自定义tabs功能 2025-07-24 15:21:08 +08:00
da149760cb feat: 🚀 tabs功能基本完成 2025-07-23 15:42:28 +08:00
d79c3f8191 feat: 🚀 优化编辑器自定义图片和视频命名 2025-07-22 16:13:26 +08:00
2347bc6f0c feat: 🚀 不知道修复了没有 2025-07-22 14:26:12 +08:00
f867f50114 feat: 🚀 文章分类添加图片 2025-07-18 15:05:07 +08:00
fd0aaee998 feat: 🚀 图片排序 2025-07-17 15:51:07 +08:00
e2261c5fc4 feat: 🚀 图片排序 2025-07-17 15:20:57 +08:00
cc894333bf feat: 🚀 批量图片排序 2025-07-17 10:48:59 +08:00
e24a4adec6 feat: 🚀 图片上传按顺序回显 2025-07-16 15:36:48 +08:00
ebef3963f8 feat: 🚀 优化富文本多张图片上传 2025-07-14 17:53:37 +08:00
0ecb7691da feat: 🚀 登出地址 2025-07-14 10:57:34 +08:00
9c2253a91e feat: 🚀 生产环境登录地址修改 2025-07-14 10:30:19 +08:00
55 changed files with 5277 additions and 427 deletions

View File

@@ -2,7 +2,7 @@ ENV = 'dev'
VITE_APP_API_BASEURL =https://dev.ow.f2b211.com/admapi/v1 VITE_APP_API_BASEURL =https://dev.ow.f2b211.com/admapi/v1
#前端回显图片之类的地址 #前端回显图片之类的地址
VITE_APP_API_BASE_UPLOAD_URL =https://dev.ow.f2b211.com VITE_APP_API_BASE_UPLOAD_URL =https://dev.ow.f2b211.com/
# 本地环境 # 本地环境
VITE_USER_NODE_ENV = development VITE_USER_NODE_ENV = development

View File

@@ -1,10 +1,10 @@
ENV = 'production' ENV = 'production'
VITE_APP_API_BASEURL =https://ow.f2b211.com/admapi/v1 VITE_APP_API_BASEURL =https://orico.com.cn/admapi/v1
#前端回显图片之类的地址 #前端回显图片之类的地址
# 线上环境 # 线上环境
VITE_USER_NODE_ENV = production VITE_USER_NODE_ENV = production
#前端回显图片之类的地址 #前端回显图片之类的地址
VITE_APP_API_BASE_UPLOAD_URL =https://ow.f2b211.com/ VITE_APP_API_BASE_UPLOAD_URL =https://orico.com.cn/
# 是否启用 gzip 或 brotli 压缩打包,如果需要多个压缩规则,可以使用 “,” 分隔 # 是否启用 gzip 或 brotli 压缩打包,如果需要多个压缩规则,可以使用 “,” 分隔
# Optional: gzip | brotli | none # Optional: gzip | brotli | none

BIN
dist.zip

Binary file not shown.

11
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@vueuse/core": "^10.1.2", "@vueuse/core": "^10.1.2",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"@zhj-target/vue3-kind-editor": "^0.1.3",
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"bwip-js": "^4.3.2", "bwip-js": "^4.3.2",
@@ -3896,6 +3897,11 @@
"snabbdom": "^3.1.0" "snabbdom": "^3.1.0"
} }
}, },
"node_modules/@zhj-target/vue3-kind-editor": {
"version": "0.1.3",
"resolved": "https://registry.npmmirror.com/@zhj-target/vue3-kind-editor/-/vue3-kind-editor-0.1.3.tgz",
"integrity": "sha512-tzSutZUBmGBRFBW8UMu2w5qmFMzp0RbaER8YQnMytq3COc04BHETiQkMOQ2PCHkrUXitzjBOWNSquW/AibizQA=="
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.8.2", "version": "8.8.2",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz",
@@ -17949,6 +17955,11 @@
"integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==", "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
"requires": {} "requires": {}
}, },
"@zhj-target/vue3-kind-editor": {
"version": "0.1.3",
"resolved": "https://registry.npmmirror.com/@zhj-target/vue3-kind-editor/-/vue3-kind-editor-0.1.3.tgz",
"integrity": "sha512-tzSutZUBmGBRFBW8UMu2w5qmFMzp0RbaER8YQnMytq3COc04BHETiQkMOQ2PCHkrUXitzjBOWNSquW/AibizQA=="
},
"acorn": { "acorn": {
"version": "8.8.2", "version": "8.8.2",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz",

View File

@@ -26,6 +26,7 @@
"@vueuse/core": "^10.1.2", "@vueuse/core": "^10.1.2",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"@zhj-target/vue3-kind-editor": "^0.1.3",
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"bwip-js": "^4.3.2", "bwip-js": "^4.3.2",

View File

@@ -71,12 +71,20 @@ class RequestHttp {
*/ */
this.service.interceptors.response.use( this.service.interceptors.response.use(
(response: AxiosResponse) => { (response: AxiosResponse) => {
const { data } = response; const { data, request } = response;
tryHideFullScreenLoading(); tryHideFullScreenLoading();
const imgId = request?.responseURL?.split("imgId=")[1];
//获取导出表格名称 //获取导出表格名称
getDispositionName(response); getDispositionName(response);
// 获取响应头中的 Authorization 信息 // 获取响应头中的 Authorization 信息
const authorization = response.headers["authorization"]; const authorization = response.headers["authorization"];
if (imgId) {
return {
imgId,
data
};
}
if (authorization) { if (authorization) {
// 可以在这里更新用户的 token 信息 // 可以在这里更新用户的 token 信息
const userStore = useUserStore(); const userStore = useUserStore();

View File

@@ -19,7 +19,7 @@ export const getArticleClassDelApi = (params: any) => {
}; };
//文章分类更新(用于编辑后) //文章分类更新(用于编辑后)
export const getArticleClassEditUpApi = (params: any) => { export const getArticleClassEditUpApi = (params: any) => {
const { id, name, sort, is_show, pid, seo_title, seo_keywords, seo_desc } = params; const { id, name, sort, is_show, pid, seo_title, seo_keywords, seo_desc, icon } = params;
return http.put<any>(`/article/category/update/${id}`, { return http.put<any>(`/article/category/update/${id}`, {
name, name,
@@ -28,7 +28,8 @@ export const getArticleClassEditUpApi = (params: any) => {
pid, pid,
seo_title, seo_title,
seo_keywords, seo_keywords,
seo_desc seo_desc,
icon
}); });
}; };
//文章分类详情(用于编辑) //文章分类详情(用于编辑)

View File

@@ -11,8 +11,8 @@ import http from "@/api";
* @name 文件上传模块 * @name 文件上传模块
*/ */
// 图片上传 // 图片上传
export const uploadImg = (params: any, name?: any) => { export const uploadImg = (params: any, name?: any, id?: any) => {
return http.post<any>(`/images/${name}/upload`, params); return http.post<any>(`/images/${name}/upload?imgId=${id}`, params);
}; };
// 视频上传 // 视频上传

117
src/auto-import.d.ts vendored
View File

@@ -4,67 +4,66 @@
// Generated by unplugin-auto-import // Generated by unplugin-auto-import
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import("vue")["EffectScope"];
const ElNotification: typeof import('element-plus/es')['ElNotification'] const computed: typeof import("vue")["computed"];
const computed: typeof import('vue')['computed'] const createApp: typeof import("vue")["createApp"];
const createApp: typeof import('vue')['createApp'] const customRef: typeof import("vue")["customRef"];
const customRef: typeof import('vue')['customRef'] const defineAsyncComponent: typeof import("vue")["defineAsyncComponent"];
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] const defineComponent: typeof import("vue")["defineComponent"];
const defineComponent: typeof import('vue')['defineComponent'] const effectScope: typeof import("vue")["effectScope"];
const effectScope: typeof import('vue')['effectScope'] const getCurrentInstance: typeof import("vue")["getCurrentInstance"];
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import("vue")["getCurrentScope"];
const getCurrentScope: typeof import('vue')['getCurrentScope'] const h: typeof import("vue")["h"];
const h: typeof import('vue')['h'] const inject: typeof import("vue")["inject"];
const inject: typeof import('vue')['inject'] const isProxy: typeof import("vue")["isProxy"];
const isProxy: typeof import('vue')['isProxy'] const isReactive: typeof import("vue")["isReactive"];
const isReactive: typeof import('vue')['isReactive'] const isReadonly: typeof import("vue")["isReadonly"];
const isReadonly: typeof import('vue')['isReadonly'] const isRef: typeof import("vue")["isRef"];
const isRef: typeof import('vue')['isRef'] const markRaw: typeof import("vue")["markRaw"];
const markRaw: typeof import('vue')['markRaw'] const nextTick: typeof import("vue")["nextTick"];
const nextTick: typeof import('vue')['nextTick'] const onActivated: typeof import("vue")["onActivated"];
const onActivated: typeof import('vue')['onActivated'] const onBeforeMount: typeof import("vue")["onBeforeMount"];
const onBeforeMount: typeof import('vue')['onBeforeMount'] const onBeforeRouteLeave: typeof import("vue-router")["onBeforeRouteLeave"];
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] const onBeforeRouteUpdate: typeof import("vue-router")["onBeforeRouteUpdate"];
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] const onBeforeUnmount: typeof import("vue")["onBeforeUnmount"];
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] const onBeforeUpdate: typeof import("vue")["onBeforeUpdate"];
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] const onDeactivated: typeof import("vue")["onDeactivated"];
const onDeactivated: typeof import('vue')['onDeactivated'] const onErrorCaptured: typeof import("vue")["onErrorCaptured"];
const onErrorCaptured: typeof import('vue')['onErrorCaptured'] const onMounted: typeof import("vue")["onMounted"];
const onMounted: typeof import('vue')['onMounted'] const onRenderTracked: typeof import("vue")["onRenderTracked"];
const onRenderTracked: typeof import('vue')['onRenderTracked'] const onRenderTriggered: typeof import("vue")["onRenderTriggered"];
const onRenderTriggered: typeof import('vue')['onRenderTriggered'] const onScopeDispose: typeof import("vue")["onScopeDispose"];
const onScopeDispose: typeof import('vue')['onScopeDispose'] const onServerPrefetch: typeof import("vue")["onServerPrefetch"];
const onServerPrefetch: typeof import('vue')['onServerPrefetch'] const onUnmounted: typeof import("vue")["onUnmounted"];
const onUnmounted: typeof import('vue')['onUnmounted'] const onUpdated: typeof import("vue")["onUpdated"];
const onUpdated: typeof import('vue')['onUpdated'] const provide: typeof import("vue")["provide"];
const provide: typeof import('vue')['provide'] const reactive: typeof import("vue")["reactive"];
const reactive: typeof import('vue')['reactive'] const readonly: typeof import("vue")["readonly"];
const readonly: typeof import('vue')['readonly'] const ref: typeof import("vue")["ref"];
const ref: typeof import('vue')['ref'] const resolveComponent: typeof import("vue")["resolveComponent"];
const resolveComponent: typeof import('vue')['resolveComponent'] const shallowReactive: typeof import("vue")["shallowReactive"];
const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReadonly: typeof import("vue")["shallowReadonly"];
const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowRef: typeof import("vue")["shallowRef"];
const shallowRef: typeof import('vue')['shallowRef'] const toRaw: typeof import("vue")["toRaw"];
const toRaw: typeof import('vue')['toRaw'] const toRef: typeof import("vue")["toRef"];
const toRef: typeof import('vue')['toRef'] const toRefs: typeof import("vue")["toRefs"];
const toRefs: typeof import('vue')['toRefs'] const toValue: typeof import("vue")["toValue"];
const toValue: typeof import('vue')['toValue'] const triggerRef: typeof import("vue")["triggerRef"];
const triggerRef: typeof import('vue')['triggerRef'] const unref: typeof import("vue")["unref"];
const unref: typeof import('vue')['unref'] const useAttrs: typeof import("vue")["useAttrs"];
const useAttrs: typeof import('vue')['useAttrs'] const useCssModule: typeof import("vue")["useCssModule"];
const useCssModule: typeof import('vue')['useCssModule'] const useCssVars: typeof import("vue")["useCssVars"];
const useCssVars: typeof import('vue')['useCssVars'] const useLink: typeof import("vue-router")["useLink"];
const useLink: typeof import('vue-router')['useLink'] const useRoute: typeof import("vue-router")["useRoute"];
const useRoute: typeof import('vue-router')['useRoute'] const useRouter: typeof import("vue-router")["useRouter"];
const useRouter: typeof import('vue-router')['useRouter'] const useSlots: typeof import("vue")["useSlots"];
const useSlots: typeof import('vue')['useSlots'] const watch: typeof import("vue")["watch"];
const watch: typeof import('vue')['watch'] const watchEffect: typeof import("vue")["watchEffect"];
const watchEffect: typeof import('vue')['watchEffect'] const watchPostEffect: typeof import("vue")["watchPostEffect"];
const watchPostEffect: typeof import('vue')['watchPostEffect'] const watchSyncEffect: typeof import("vue")["watchSyncEffect"];
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
} }
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue' export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from "vue";
} }

View File

@@ -24,7 +24,7 @@
} }
.ql-snow .ql-picker.ql-size .ql-picker-label::before, .ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before { .ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: "14px"; content: "12px";
} }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before, .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before { .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
@@ -78,3 +78,150 @@
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before { .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
content: "等宽字体"; content: "等宽字体";
} }
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="SimSun"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="SimSun"]::before {
font-family: SimSun, sans-serif;
content: "宋体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="SimHei"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="SimHei"]::before {
font-family: SimHei, sans-serif;
content: "黑体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="Microsoft-YaHei"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="Microsoft-YaHei"]::before {
font-family: "Microsoft YaHei", sans-serif;
content: "微软雅黑";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="KaiTi"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="KaiTi"]::before {
font-family: KaiTi, sans-serif;
content: "楷体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="FangSong"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="FangSong"]::before {
font-family: FangSong, sans-serif;
content: "仿宋";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="Arial"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="Arial"]::before {
font-family: Arial, sans-serif;
content: "Arial";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="Times-New-Roman"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="Times-New-Roman"]::before {
font-family: "Times New Roman", sans-serif;
content: "Times New Roman";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="sans-serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="sans-serif"]::before {
font-family: sans-serif;
content: "sans-serif";
}
.ql-font-SimSun {
font-family: SimSun, sans-serif;
}
.ql-font-SimHei {
font-family: SimHei, sans-serif;
}
.ql-font-Microsoft-YaHei {
font-family: "Microsoft YaHei", sans-serif;
}
.ql-font-KaiTi {
font-family: KaiTi, sans-serif;
}
.ql-font-FangSong {
font-family: FangSong, sans-serif;
}
.ql-font-Arial {
font-family: Arial, sans-serif;
}
.ql-font-Times-New-Roman {
font-family: "Times New Roman", sans-serif;
}
.ql-font-sans-serif {
font-family: sans-serif;
}
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="10px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before {
// content: "10px";
// }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before {
content: "12px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {
content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
content: "16px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {
content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
content: "20px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="22px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="22px"]::before {
content: "22px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="24px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before {
content: "24px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="26px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="26px"]::before {
content: "26px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="28px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="28px"]::before {
content: "28px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="30px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="30px"]::before {
content: "30px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="32px"]::before,
.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="32px"]::before,
// .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="44px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="44px"]::before {
// content: "44px";
// }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="45px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="45px"]::before {
// content: "45px";
// }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="50px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="50px"]::before {
// content: "50px";
// }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,960 @@
<template>
<!-- 图片上传组件 -->
<el-upload
:id="uuid"
action="#"
:multiple="true"
:show-file-list="false"
:http-request="handleHttpUpload"
:before-upload="handleBeforeUpload"
class="editor-img-uploader"
accept=".jpeg,.jpg,.png,.gif"
>
<i ref="uploadRef" class="Plus editor-img-uploader"></i>
</el-upload>
<!-- 视频上传组件 -->
<input
type="file"
accept="video/*"
name="file"
ref="uploadFileVideo"
id="uploadFileVideo"
@change="handleVideoUpload"
style="width: 0; height: 0; cursor: pointer; opacity: 0"
/>
<!-- 主富文本编辑器 -->
<div class="editor">
<QuillEditor
id="mainEditor"
ref="myQuillEditor"
v-model:content="editorContent"
contentType="html"
@update:content="onContentChange"
:options="options"
/>
</div>
<!-- 标签页配置弹窗 -->
<div>
<el-dialog
v-model="outerVisible"
title="标签页配置"
style="width: 1200px; height: 900px"
close-on-click-modal
close-on-press-escape
:before-close="handleBeforeClose"
>
<el-tabs
v-model="activeName"
type="card"
class="demo-tabs"
editable
@edit="handleTabsEdit"
@tab-change="handleTabChange"
>
<!-- 标签页标题支持编辑 -->
<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">
<!-- 文字显示状态 -->
<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"
@blur="finishEditTitle(index)"
/>
</div>
</template>
<!-- 标签页编辑器内容 -->
<QuillEditor
:id="`tabEditor_${item.key}`"
:ref="
el => {
if (el) tabEditors[index] = el;
}
"
v-model:content="item.content"
contentType="html"
:options="options1"
/>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleQX">取消</el-button>
<el-button type="primary" @click="handleQR"> 确认 </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Editor">
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";
// 字体配置
let fontSizeStyle = Quill.import("attributors/style/size");
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"]);
const uuid = ref("id-" + generateUUID());
const $router = useRouter();
const routerValueName = $router.currentRoute.value.name;
const routerName = ref(routerObj[routerValueName]);
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-索引
// 标签页数据新增key作为唯一标识isEditing控制编辑状态
const tabsData = ref([]);
// 标签页编辑器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: 5 }
});
// 主编辑器内容双向绑定
const editorContent = computed({
get: () => {
if (!props.content) return "";
return props.content;
},
set: val => {
emit("update:content", val);
}
});
const myQuillEditor = ref(null); // 主编辑器ref
// 主编辑器配置(保持不变)
const options = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
["bold", "italic", "underline", "strike"],
["blockquote", "code-block"],
[{ list: "ordered" }, { list: "bullet" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ size: fontSizeStyle.whitelist }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ align: [] }],
["clean"],
["link", "image", "video", "tabs"]
],
handlers: {
image: function (value) {
if (value) {
activeEditor.value = "main";
proxy.$refs.uploadRef.click();
} else Quill.format("customImage", true);
},
video: function (value) {
if (value) {
activeEditor.value = "main";
document.querySelector("#uploadFileVideo")?.click();
} else Quill.format("customVideo", true);
},
tabs: function (value) {
outerVisible.value = value;
}
}
}
},
placeholder: "请输入内容...",
readOnly: props.readOnly
});
// 标签页编辑器配置(保持不变)
const options1 = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
["bold", "italic", "underline", "strike"],
["blockquote", "code-block"],
[{ list: "ordered" }, { list: "bullet" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ size: fontSizeStyle.whitelist }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ align: [] }],
["clean"],
["link", "image", "video"]
],
handlers: {
image: function (value) {
if (value) {
const currentIndex = tabsData.value.findIndex(item => item.key === activeName.value);
activeEditor.value = `tab-${currentIndex}`;
proxy.$refs.uploadRef.click();
} else Quill.format("customImage", true);
},
video: function (value) {
if (value) {
const currentIndex = tabsData.value.findIndex(item => item.key === activeName.value);
activeEditor.value = `tab-${currentIndex}`;
document.querySelector("#uploadFileVideo")?.click();
} else Quill.format("customVideo", true);
}
}
}
},
placeholder: "请输入内容...",
readOnly: props.readOnly
});
// 上传前校验(保持不变)
const handleBeforeUpload = file => {
const fileType = file.type;
file.customUid = generateUUID();
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;
}
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);
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);
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 = "";
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();
// 关键修改:按照原始上传顺序排序
// 假设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()
});
});
// 最终光标定位到最后一张图片后面
const finalPosition = insertPosition + sortedImages.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 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 file = evt.target.files[0];
// 1. 校验视频文件
const maxSize = 150 * 1024 * 1024;
if (file.size > maxSize) {
ElNotification({
title: "文件过大",
message: `视频大小不能超过 ${150}MB`,
type: "warning"
});
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, "==============");
} 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);
activeName.value = key;
activeEditor.value = `tab-${tabIndex}`;
};
// 标签页增删事件
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;
tabsData.value.push({
key: newKey,
title: `标签${newIndex + 1}`,
content: "",
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;
}
}
}
};
// 开始编辑标签页标题
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; // 触发重绘
}
};
// 其他方法(保持不变)
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;
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 = () => {
setTabsInfo();
};
const initTitle = () => {
const editor = document.querySelector(".ql-editor");
if (editor) editor.dataset.placeholder = "";
titleConfig.value.forEach(item => {
const tip = document.querySelector(`.ql-toolbar ${item.Choice}`);
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();
if (quill) {
quill.setText("");
editorContent.value = "";
}
}
});
</script>
<style lang="scss">
@import "./index.scss";
// 增加编辑器内容区交互性确保删除可用
.ql-editor {
min-height: 600px; // 确保空编辑器也有点击区域
cursor: text !important;
user-select: text !important;
}
// /* 标签页样式 */
.quill-tabs {
margin: 15px 0;
overflow: hidden;
border-radius: 4px;
}
// /* 用伪元素添加图标(可替换为自己的图标) */
.ql-tabs::before {
font-size: 16px;
content: "T"; /* 用 emoji 或字体图标 */
}
.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

@@ -5,9 +5,14 @@ class ImageBlot extends BlockEmbed {
let node = super.create(); let node = super.create();
node.setAttribute("src", value.url); node.setAttribute("src", value.url);
node.setAttribute("id", value.id); node.setAttribute("id", value.id);
console.log("图片信息", node);
return node; return node;
} }
// 允许通过键盘删除
deleteAt(index, length) {
super.deleteAt(index, length);
}
static value(node) { static value(node) {
return { return {
url: node.getAttribute("src"), url: node.getAttribute("src"),
@@ -15,6 +20,6 @@ class ImageBlot extends BlockEmbed {
}; };
} }
} }
ImageBlot.blotName = "image"; ImageBlot.blotName = "customImage";
ImageBlot.tagName = "img"; ImageBlot.tagName = "img";
export default ImageBlot; export default ImageBlot;

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

@@ -0,0 +1,407 @@
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 = "m-quill-tabs";
constructor(domNode) {
super(domNode);
this.bindEvents();
this.bindDeleteKeyEvent(); // 绑定删除键事件
}
static create(value) {
const node = super.create(value);
const tabs = value || [];
// 主容器样式
node.setAttribute(
"style",
`
margin: 15px 0;
overflow: hidden;
border-radius: 4px;
position: relative;
`
);
// 标签栏滚动容器 - 优化高度计算
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: 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(
"style",
`
padding: 15px;
`
);
// 生成内容面板 - 保持原有样式不变
tabs.forEach((tab, index) => {
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);
});
// 组装结构
tabScrollContainer.appendChild(tabList);
node.appendChild(tabScrollContainer); // 滚动容器添加到主节点
node.appendChild(contentList); // 内容区
// 标签页切换逻辑 - 保持原有逻辑不变
const scriptTag = document.createElement("script");
scriptTag.textContent = `
(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) {
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(scriptTag);
node.setAttribute("contenteditable", "false");
return node;
}
bindEvents() {
if (!this.eventBoundElements) {
this.eventBoundElements = new WeakMap();
}
// 编辑按钮事件 - 保持原有逻辑不变
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", {
bubbles: true,
detail: { blot: this }
})
);
};
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)) {
btn.addEventListener("click", () => {
const index = parseInt(btn.dataset.index, 10);
this.selectTab(index);
});
this.eventBoundElements.set(btn, true);
}
});
}
// 增强版删除键处理 - 保持原有逻辑不变
bindDeleteKeyEvent() {
this.domNode.addEventListener(
"keydown",
e => {
if (e.key === "Backspace" || e.key === "Delete") {
const selection = window.getSelection();
if (!selection.rangeCount) return;
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();
}
}
},
true
);
const tabList = this.domNode.querySelector(".m-quill-tab-list");
if (tabList) {
tabList.querySelectorAll("*").forEach(el => {
el.contentEditable = "false";
});
}
}
selectTab(index) {
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",
`
font-weight: 900;
cursor: pointer;
background: transparent;
border: none;
font-size:16px;
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;" : ""}
`
);
});
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(".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 || ""
});
});
return tabs;
}
update(mutations, context) {
super.update(mutations, context);
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();
}
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",
`
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);
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();
}
}
export default TabsBlot;

View File

@@ -0,0 +1,398 @@
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 = "m-quill-tabs";
constructor(domNode) {
super(domNode);
this.bindEvents();
this.bindDeleteKeyEvent(); // 绑定删除键事件
}
static create(value) {
const node = super.create(value);
const tabs = value || [];
// 主容器样式
node.setAttribute(
"style",
`
margin: 15px 0;
overflow: hidden;
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 = "m-quill-tab-list";
tabList.setAttribute(
"style",
`
display: flex;
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 = "m-quill-tab-content-list";
contentList.setAttribute(
"style",
`
padding: 15px;
`
);
// 生成内容面板 - 保持原有样式不变
tabs.forEach((tab, index) => {
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);
});
// 组装结构
tabScrollContainer.appendChild(tabList);
node.appendChild(tabScrollContainer); // 滚动容器添加到主节点
node.appendChild(contentList); // 内容区
// 标签页切换逻辑 - 保持原有逻辑不变
const scriptTag = document.createElement("script");
scriptTag.textContent = `
(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'; // 管理系统强制显示按钮
}
// 非管理系统才执行标签切换逻辑(管理系统不执行)
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';
});
});
});
}
})();
`;
node.appendChild(scriptTag);
node.setAttribute("contenteditable", "false");
return node;
}
bindEvents() {
if (!this.eventBoundElements) {
this.eventBoundElements = new WeakMap();
}
// 编辑按钮事件 - 保持原有逻辑不变
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", {
bubbles: true,
detail: { blot: this }
})
);
};
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)) {
btn.addEventListener("click", () => {
const index = parseInt(btn.dataset.index, 10);
this.selectTab(index);
});
this.eventBoundElements.set(btn, true);
}
});
}
// 增强版删除键处理 - 保持原有逻辑不变
bindDeleteKeyEvent() {
this.domNode.addEventListener(
"keydown",
e => {
if (e.key === "Backspace" || e.key === "Delete") {
const selection = window.getSelection();
if (!selection.rangeCount) return;
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();
}
}
},
true
);
const tabList = this.domNode.querySelector(".m-quill-tab-list");
if (tabList) {
tabList.querySelectorAll("*").forEach(el => {
el.contentEditable = "false";
});
}
}
selectTab(index) {
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: 1%;
font-weight: 900;
cursor: pointer;
background: transparent;
border: none;
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;" : ""}
`
);
});
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(".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 || ""
});
});
return tabs;
}
update(mutations, context) {
super.update(mutations, context);
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();
}
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();
}
}
export default TabsBlot;

View File

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

@@ -28,7 +28,7 @@
</template> </template>
<template #default="scope" v-if="item.formType === 'inputNumber'"> <template #default="scope" v-if="item.formType === 'inputNumber'">
<el-input-number <el-input-number
:min="1" :min="0"
:max="9999" :max="9999"
:controls="true" :controls="true"
style="width: 125px" style="width: 125px"

View File

@@ -77,9 +77,9 @@
:check-strictly="false" :check-strictly="false"
show-checkbox show-checkbox
style="width: 224px" style="width: 224px"
clearable ref="treeSelectRef"
:collapse-tags="true"
@change="handleTreeSelectChange(item)" @change="handleTreeSelectChange(item)"
@remove-tag="handleRemoveTag(item, $event)"
/> />
</template> </template>
<!-- 双 --> <!-- 双 -->
@@ -116,7 +116,7 @@
/> />
</template> </template>
<template v-if="item.type === 'selectInputs'"> <!-- <template v-if="item.type === 'selectInputs'">
<div></div> <div></div>
<el-select <el-select
v-model="selectInputValue" v-model="selectInputValue"
@@ -151,19 +151,19 @@
style="width: 106px !important" style="width: 106px !important"
@input="handleInput(item)" @input="handleInput(item)"
/> />
</template> </template> -->
</div> </div>
</template> </template>
<script lang="ts" setup name="SearchFormItem"> <script lang="ts" setup name="SearchFormItem">
// import { verificationInput } from "./utils/verificationInput"; // import { verificationInput } from "./utils/verificationInput";
import { getCountryListApi } from "@/api/modules/global"; import { getCountryListApi } from "@/api/modules/global";
import $Bus from "@/utils/mittBus";
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
import { ref } from "vue"; // import { ref } from "vue";
const $router = useRouter(); const $router = useRouter();
const routeName: any = ref($router.currentRoute.value.name); const routeName: any = ref($router.currentRoute.value.name);
const treeSelectRef = ref<any>(null);
// const userStore: any = useUserStore(); // const userStore: any = useUserStore();
interface SearchFormItem { interface SearchFormItem {
item: { [key: string]: any }; item: { [key: string]: any };
@@ -171,17 +171,8 @@ interface SearchFormItem {
search: (params: any) => void; // 搜索方法 search: (params: any) => void; // 搜索方法
handleEmitClear?: (item: any) => void; handleEmitClear?: (item: any) => void;
} }
let selectInputValue = ref(1); // let selectInputValue = ref();
const options = [
{
value: 1,
label: "序号"
},
{
value: 2,
label: "数字序列号"
}
];
// const treeSelectValue = ref(null); // const treeSelectValue = ref(null);
const props = defineProps<SearchFormItem>(); const props = defineProps<SearchFormItem>();
const _searchParam = computed(() => props.searchParam); const _searchParam = computed(() => props.searchParam);
@@ -220,7 +211,119 @@ const remoteMethod = async (query: any, item: any) => {
} }
loading.value = false; 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) => { 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) { if (_searchParam.value[item.prop].length) {
let values = cloneDeep(_searchParam.value[item.prop]); let values = cloneDeep(_searchParam.value[item.prop]);
_searchParam.value[item.prop1] = values.join(","); _searchParam.value[item.prop1] = values.join(",");
@@ -238,27 +341,17 @@ const handleInput = (item: any) => {
//验证 //验证
// verificationInput(item, _searchParam, selectInputValue.value); // verificationInput(item, _searchParam, selectInputValue.value);
}; };
const handleChange = (item: any) => { // const handleChange = (item: any) => {
_searchParam.value[item.endProp] = ""; // _searchParam.value[item.endProp] = "";
_searchParam.value[item.startProp] = ""; // _searchParam.value[item.startProp] = "";
_searchParam.value["serialNumberBegin"] = ""; // _searchParam.value["serialNumberBegin"] = "";
_searchParam.value["numberCodeBegin"] = ""; // _searchParam.value["numberCodeBegin"] = "";
_searchParam.value["serialNumberEnd"] = ""; // _searchParam.value["serialNumberEnd"] = "";
_searchParam.value["numberCodeEnd"] = ""; // _searchParam.value["numberCodeEnd"] = "";
}; // };
const handleEmitClear = (item: any) => { const handleEmitClear = (item: any) => {
console.log(item); 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> </script>
<style lang="scss" scope> <style lang="scss" scope>

View File

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

View File

@@ -119,9 +119,9 @@ const handleHttpUpload = async (options: UploadRequestOptions) => {
const api = props.api ?? uploadImg; const api = props.api ?? uploadImg;
const result = await api(formData, routerName.value); const result = await api(formData, routerName.value);
if (result?.code === 0) { console.log(result, "============>>>");
const { data } = result; if (result?.data?.code === 0) {
emit("update:imageUrl", data.path); emit("update:imageUrl", result?.data?.data.path);
} }
// 调用 el-form 内部的校验方法(可自动校验) // 调用 el-form 内部的校验方法(可自动校验)

View File

@@ -43,10 +43,9 @@ import { ref, computed, inject, watch } from "vue";
import { Plus, Delete } from "@element-plus/icons-vue"; import { Plus, Delete } from "@element-plus/icons-vue";
import { uploadImg } from "@/api/modules/upload"; import { uploadImg } from "@/api/modules/upload";
import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus"; import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
import { generateUUID } from "@/utils"; // import { generateUUID } from "@/utils";
import { ElNotification, formContextKey, formItemContextKey } from "element-plus"; import { ElNotification, formContextKey, formItemContextKey } from "element-plus";
let uid = generateUUID(); // let uid = generateUUID();
console.log("uid:", uid);
interface UploadFileProps { interface UploadFileProps {
fileList: UploadUserFile[]; fileList: UploadUserFile[];
api?: (params: any) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传 api?: (params: any) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
@@ -134,9 +133,10 @@ const handleHttpUpload = async (options: UploadRequestOptions) => {
formData.append("image", options.file); formData.append("image", options.file);
try { try {
const api = props.api ?? uploadImg; const api = props.api ?? uploadImg;
const { data } = await api(formData, routerName.value); const { data } = await api(formData, routerName.value);
console.log(data.path, "========data=========="); console.log(data.data.path, "========data==========");
options.onSuccess(data.path); options.onSuccess(data.data.path);
} catch (error) { } catch (error) {
options.onError(error as any); options.onError(error as any);
} }

View File

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

View File

@@ -29,7 +29,6 @@ const handleCreated = (editor: any) => {
editorRef.value = editor; editorRef.value = editor;
}; };
console.log(editorRef.value, "=editorRef.value=");
const $router = useRouter(); const $router = useRouter();
const routerValueName: string = $router.currentRoute.value.name as string; const routerValueName: string = $router.currentRoute.value.name as string;
const routerObj: any = { const routerObj: any = {

View File

@@ -53,7 +53,7 @@
</template> </template>
<template v-if="item.type === 'inputNumber'"> <template v-if="item.type === 'inputNumber'">
<el-input-number <el-input-number
:min="1" :min="0"
:max="9999" :max="9999"
:controls="true" :controls="true"
v-model="_ruleForm[`${item.prop}`]" v-model="_ruleForm[`${item.prop}`]"
@@ -178,7 +178,7 @@
<slot /> <slot />
</template> </template>
<template v-if="item.type === 'WangEditor'"> <template v-if="item.type === 'WangEditor'">
<Editor v-model:content="_ruleForm[`${item.prop}`]"></Editor> <Editor v-model:content="_ruleForm[`${item.prop}`]" ref="editorRef"></Editor>
</template> </template>
</el-form-item> </el-form-item>
</template> </template>
@@ -209,7 +209,7 @@ const emits = defineEmits<{
(e: "handleRadioGroupEmits", result?: any): void; (e: "handleRadioGroupEmits", result?: any): void;
(e: "handleTreesSelectChangeEmits", result?: any): void; (e: "handleTreesSelectChangeEmits", result?: any): void;
}>(); }>();
const editorRef = ref<any>(null);
const ruleFormRef = ref<FormInstance>(); const ruleFormRef = ref<FormInstance>();
const props = defineProps<IProps>(); const props = defineProps<IProps>();
//本地化处理,props是单向的,通过本地化就可以改变父组件传过来的值了 //本地化处理,props是单向的,通过本地化就可以改变父组件传过来的值了
@@ -235,7 +235,8 @@ const handleRadioGroup = (value: any) => {
// 暴露给父组件的参数和方法(外部需要什么,都可以从这里暴露出去) // 暴露给父组件的参数和方法(外部需要什么,都可以从这里暴露出去)
defineExpose({ defineExpose({
ruleForm: _ruleForm, ruleForm: _ruleForm,
ruleFormRef: ruleFormRef ruleFormRef: ruleFormRef,
editorRef: editorRef
}); });
</script> </script>

View File

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

View File

@@ -44,9 +44,16 @@ import { useMsg } from "@/hooks/useMsg";
import { useUserStore } from "@/stores/modules/user"; import { useUserStore } from "@/stores/modules/user";
import { ElMessageBox } from "element-plus"; import { ElMessageBox } from "element-plus";
import { outLogin } from "@/utils/outLogin"; import { outLogin } from "@/utils/outLogin";
import { useKeepAliveStore } from "@/stores/modules/keepAlive";
import { getLanguageListApi, getLanguageCutoverApi } from "@/api/modules/global"; 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 userStore = useUserStore();
const router = useRouter();
const keepAliveStore = useKeepAliveStore();
document.cookie = `lang=zh_cn`; document.cookie = `lang=zh_cn`;
const langs = ref<any>([]); const langs = ref<any>([]);
const name = ref(""); const name = ref("");
@@ -58,13 +65,13 @@ const getLanguageList = async () => {
const { data } = result; const { data } = result;
langs.value = data; langs.value = data;
let id = userStore?.languageType ? userStore?.languageType : data[0]?.id; let id = userStore?.languageType ? userStore?.languageType : data[0]?.id;
getLanguageCutover(id); getLanguageCutover(id, "noCLick");
} }
}; };
getLanguageList(); getLanguageList();
// 站点切换接口 // 站点切换接口
const getLanguageCutover = async (id: any) => { const getLanguageCutover = async (id: any, type: any) => {
const result = await getLanguageCutoverApi(id); const result = await getLanguageCutoverApi(id);
if (result?.code === 0) { if (result?.code === 0) {
userStore.setLanguageType(id); userStore.setLanguageType(id);
@@ -72,12 +79,35 @@ const getLanguageCutover = async (id: any) => {
return item.id === id; return item.id === id;
}); });
name.value = names[0]?.country_name; 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) => { 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) => { const handleOpenPage = (item: any) => {
console.log(item, "===========item==========");
$router.push({ $router.push({
path: item.path path: item.path
}); });

View File

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

View File

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

View File

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

View File

@@ -271,5 +271,5 @@
white-space: nowrap; white-space: nowrap;
} }
.el-message__wrapper { .el-message__wrapper {
z-index: 9999; /* 一个较大的值,确保在抽屉之上 */ z-index: 3000 !important; /* 一个较大的值,确保在抽屉之上 */
} }

View File

@@ -4,8 +4,8 @@ import { useUserStore } from "@/stores/modules/user";
//不同环境的login地址 //不同环境的login地址
const LOGIN_OBJ: any = { const LOGIN_OBJ: any = {
development: "http://localhost:8080/admin/login", //开发环境 development: "http://localhost:8080/admin/login", //开发环境
test: "https://dev.ow.admin.f2b211.com/", //测试环境 test: "https://dev.orico.com.cn/admin/login", //测试环境
production: "http://localhost:8080/login" //生产环境 production: "https://orico.com.cn/admin/login" //生产环境
}; };
/** /**
/** /**

View File

@@ -20,7 +20,7 @@
<el-tab-pane label="问答详细" name="third"> <el-tab-pane label="问答详细" name="third">
<div style="width: 1280px; margin: 0 auto"> <div style="width: 1280px; margin: 0 auto">
<Editor v-model:content="dataStore.editRuleForm.answer" /> <Editor v-model:content="dataStore.editRuleForm.answer" ref="editorRef" />
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@@ -47,6 +47,7 @@ const dataStore = reactive<any>({
rules: RULES rules: RULES
}); });
const formRef: any = ref(null); const formRef: any = ref(null);
const editorRef = ref<any>(null);
//详情 //详情
const getQAListDetails = async () => { const getQAListDetails = async () => {
let id = $route.query.id; let id = $route.query.id;
@@ -58,6 +59,9 @@ const getQAListDetails = async () => {
const { data } = result; const { data } = result;
//这里是传给基本信息组件的表单数据 //这里是传给基本信息组件的表单数据
dataStore.editRuleForm = cloneDeep(data); dataStore.editRuleForm = cloneDeep(data);
if (!data.answer) {
editorRef?.value?.clearEditor();
}
// dataStore.editRuleForm.answer = convertSpanToDiv(dataStore.editRuleForm.answer); // dataStore.editRuleForm.answer = convertSpanToDiv(dataStore.editRuleForm.answer);
dataStore.value = data.answer; dataStore.value = data.answer;
@@ -93,9 +97,11 @@ const handleReset = () => {
const resetFields = () => { const resetFields = () => {
if (!formRef.value!.ruleFormRef) return; if (!formRef.value!.ruleFormRef) return;
formRef!.value!.ruleFormRef.resetFields(); formRef!.value!.ruleFormRef.resetFields();
editorRef?.value?.clearEditor();
for (let key in dataStore.editRuleForm) { for (let key in dataStore.editRuleForm) {
dataStore.editRuleForm[key] = ""; dataStore.editRuleForm[key] = "";
} }
dataStore.editRuleForm.sort = 0;
// dataStore.value = ""; // dataStore.value = "";
}; };

View File

@@ -53,13 +53,19 @@ export const EDIT_FORM_DATA: FormItem[] = [
type: "treeSelect", type: "treeSelect",
label: "所属分类: ", label: "所属分类: ",
options: [] options: []
},
{
prop: "icon",
type: "upImg",
label: "图片: "
} }
]; ];
export const EDIT_RULE_FORM = { export const EDIT_RULE_FORM = {
is_show: 1, is_show: 1,
sort: 1, sort: 0,
name: "", name: "",
pid: 0 pid: 0,
icon: ""
}; };
// editRuleForm: {}, // editRuleForm: {},
//editFormData: [], //editFormData: [],

View File

@@ -76,6 +76,7 @@
<script setup lang="ts" name="articleClassListIndex"> <script setup lang="ts" name="articleClassListIndex">
import rulesForm from "@/components/rulesForm/index.vue"; import rulesForm from "@/components/rulesForm/index.vue";
import SearchForm from "@/components/SearchForm/index.vue";
import { integerRexg } from "@/utils/regexp/index"; import { integerRexg } from "@/utils/regexp/index";
import { addLabelValue } from "@/utils/addLabelValue"; import { addLabelValue } from "@/utils/addLabelValue";
import { h } from "@/utils/url"; import { h } from "@/utils/url";
@@ -174,7 +175,6 @@ const getArticleClassAddSave = async () => {
}; };
//文章编辑 //文章编辑
const getArticleClassUpEdit = async () => { const getArticleClassUpEdit = async () => {
console.log("编辑");
const result = await getArticleClassEditUpApi(dataStore.editRuleForm); const result = await getArticleClassEditUpApi(dataStore.editRuleForm);
const { msg, code } = result; const { msg, code } = result;

View File

@@ -20,7 +20,7 @@
<el-tab-pane label="详细内容" name="third"> <el-tab-pane label="详细内容" name="third">
<div style="width: 1280px; margin: 0 auto"> <div style="width: 1280px; margin: 0 auto">
<Editor v-model:content="dataStore.editRuleForm.content"></Editor> <Editor v-model:content="dataStore.editRuleForm.content" ref="editorRef"></Editor>
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@@ -47,7 +47,7 @@ import { useMsg } from "@/hooks/useMsg";
const formRef = ref<any>(null); const formRef = ref<any>(null);
const activeName = ref("basicInfo"); const activeName = ref("basicInfo");
const $route = useRoute(); const $route = useRoute();
const editorRef = ref<any>(null);
//数据集合 //数据集合
const dataStore = reactive<any>({ const dataStore = reactive<any>({
rules: cloneDeep(RULES), //抽屉表单验证 rules: cloneDeep(RULES), //抽屉表单验证
@@ -79,6 +79,9 @@ const getArticleListDetails = async () => {
if (result?.code === 0) { if (result?.code === 0) {
const { data } = result; const { data } = result;
dataStore.editRuleForm = data; dataStore.editRuleForm = data;
if (!data.content) {
editorRef?.value?.clearEditor(); // 调用子组件的清空方法
}
// dataStore.editRuleForm.content = convertSpanToDiv(dataStore.editRuleForm.content); // dataStore.editRuleForm.content = convertSpanToDiv(dataStore.editRuleForm.content);
let is = dataStore.editFormData[1].options.some((item: any) => let is = dataStore.editFormData[1].options.some((item: any) =>
recursiveCompare(item, dataStore.editRuleForm.category_id) recursiveCompare(item, dataStore.editRuleForm.category_id)
@@ -148,6 +151,13 @@ const handleConfirmClick = () => {
const handleReset = () => { const handleReset = () => {
if ($route.query.type === "add") { if ($route.query.type === "add") {
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM); dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
editorRef?.value?.clearEditor(); // 调用子组件的清空方法
// if (!data.detail) {
// dataStore.detail = "";
// editorRef.value.clearEditor(); // 调用子组件的清空方法
// } else {
// dataStore.detail = cloneDeep(data.detail);
// }
} else { } else {
getArticleListDetails(); getArticleListDetails();
} }

View File

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

View File

@@ -121,6 +121,7 @@ const dataStore = reactive<any>({
formData: FORM_DATA, // 搜索配置项 dataStore.formData formData: FORM_DATA, // 搜索配置项 dataStore.formData
visible: false, visible: false,
data: [], data: [],
type: "image",
isFirstRequest: true isFirstRequest: true
}); });
@@ -140,6 +141,7 @@ const getProductCategoryList = async () => {
const result = await getProductCategoryListApi(); const result = await getProductCategoryListApi();
if (result?.code === 0) { if (result?.code === 0) {
let dataClone: any = cloneDeep(result?.data); let dataClone: any = cloneDeep(result?.data);
console.log(dataClone, "=dataClone=");
dataStore.editFormData[7].options = addLabelValue(dataClone); dataStore.editFormData[7].options = addLabelValue(dataClone);
} }
}; };
@@ -164,6 +166,7 @@ const buildTree = (data: any, outerLinkTo: any = "") => {
}; };
// let isFirstRequest = true; // let isFirstRequest = true;
const handleRadioGroupEmits = (value: any) => { const handleRadioGroupEmits = (value: any) => {
dataStore.type = value;
if (value === "video") { if (value === "video") {
dataStore.editFormData = EDIT_FORM_DATA1; dataStore.editFormData = EDIT_FORM_DATA1;
dataStore.rules = RULES1; dataStore.rules = RULES1;
@@ -205,6 +208,7 @@ const getSystemUrls = async (node: any, resolve: any) => {
} }
}; };
const setImgOrVideo = (result: any) => { const setImgOrVideo = (result: any) => {
dataStore.type = result?.data?.type;
if (result?.data?.type === "image") { if (result?.data?.type === "image") {
dataStore.editFormData = cloneDeep(EDIT_FORM_DATA); dataStore.editFormData = cloneDeep(EDIT_FORM_DATA);
} }
@@ -224,6 +228,9 @@ const getBannerRead = async (id: any) => {
if (result?.code === 0) { if (result?.code === 0) {
setImgOrVideo(result); setImgOrVideo(result);
getProductCategoryList(); getProductCategoryList();
if (result?.data?.type === "image" && !result?.data?.desc) {
formRef?.value?.editorRef[0]?.clearEditor();
}
nextTick(() => { nextTick(() => {
dataStore.editRuleForm = result?.data; dataStore.editRuleForm = result?.data;
if (dataStore.editRuleForm.link && dataStore.editRuleForm.link_to) { if (dataStore.editRuleForm.link && dataStore.editRuleForm.link_to) {
@@ -231,7 +238,6 @@ const getBannerRead = async (id: any) => {
if (!id || !name || !link) { if (!id || !name || !link) {
return; return;
} }
console.log("1232323");
let obj: any = { let obj: any = {
label: name, // 确保这里的name是你想要显示的文本 label: name, // 确保这里的name是你想要显示的文本
value: `${dataStore.editRuleForm.link_to}` + "/" + `${id}` + "/" + `${name}`, value: `${dataStore.editRuleForm.link_to}` + "/" + `${id}` + "/" + `${name}`,
@@ -249,6 +255,7 @@ const getBannerRead = async (id: any) => {
}; };
const handleCheck = (checkedNodes: any, values: any) => { const handleCheck = (checkedNodes: any, values: any) => {
const { checkedKeys } = values; const { checkedKeys } = values;
console.log(checkedKeys, "=checkedKeys=");
if (checkedKeys.length) { if (checkedKeys.length) {
dataStore.editRuleForm.link = checkedNodes.url; dataStore.editRuleForm.link = checkedNodes.url;
dataStore.editRuleForm.link_to = checkedNodes.link_to; dataStore.editRuleForm.link_to = checkedNodes.link_to;
@@ -331,6 +338,9 @@ const handleConfirmClick = () => {
const resetFields = () => { const resetFields = () => {
if (!formRef.value!.ruleFormRef) return; if (!formRef.value!.ruleFormRef) return;
formRef!.value!.ruleFormRef.resetFields(); formRef!.value!.ruleFormRef.resetFields();
if (dataStore.type === "image") {
formRef?.value?.editorRef[0]?.clearEditor();
}
}; };
// 抽屉重置 // 抽屉重置
const handleResetClick = () => { const handleResetClick = () => {
@@ -346,7 +356,8 @@ const handleAdd = () => {
dataStore.visible = true; dataStore.visible = true;
selectedNodes.value = ""; selectedNodes.value = "";
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM); dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
(dataStore.editFormData = cloneDeep(EDIT_FORM_DATA)), // 抽屉表单配置项 dataStore.editFormData = cloneDeep(EDIT_FORM_DATA); // 抽屉表单配置项
dataStore.rules = cloneDeep(RULES);
getBannerClassEditList(); getBannerClassEditList();
// getBannerClassList(); // getBannerClassList();
getProductCategoryList(); getProductCategoryList();
@@ -354,7 +365,8 @@ const handleAdd = () => {
// 抽屉关闭前的钩子 // 抽屉关闭前的钩子
const handleBeforeClone = () => { const handleBeforeClone = () => {
dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM); dataStore.editRuleForm = cloneDeep(EDIT_RULE_FORM);
(dataStore.editFormData = cloneDeep(EDIT_FORM_DATA)), // 抽屉表单配置项 dataStore.editFormData = cloneDeep(EDIT_FORM_DATA); // 抽屉表单配置项
dataStore.rules = cloneDeep(RULES);
resetFields(); resetFields();
dataStore.visible = false; dataStore.visible = false;
dataStore.isFirstRequest = true; dataStore.isFirstRequest = true;

View File

@@ -61,7 +61,7 @@ const $route = useRoute();
import { getCategorysApi } from "@/api/modules/downloadClass"; import { getCategorysApi } from "@/api/modules/downloadClass";
import { recursiveCompare } from "@/utils/recursiveCompare"; import { recursiveCompare } from "@/utils/recursiveCompare";
import { addLabelValue } from "@/utils/addLabelValue"; import { addLabelValue } from "@/utils/addLabelValue";
import { ElNotification } from "element-plus";
//深拷贝方法 //深拷贝方法
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
import { import {
@@ -156,7 +156,6 @@ const excelUploadSuccess = (response: any, row: any) => {
if (response?.code === 0) { if (response?.code === 0) {
row.file_path = response.data.path; // 假设后端返回的文件路径在 data.path 中 row.file_path = response.data.path; // 假设后端返回的文件路径在 data.path 中
} }
console.log(row, "====row========");
ElNotification({ ElNotification({
title: "温馨提示", title: "温馨提示",
message: `文件上传成功!`, message: `文件上传成功!`,

291
src/views/login/index2.vue Normal file
View File

@@ -0,0 +1,291 @@
<template>
<div class="main">
<div class="container">
<div class="left">
<div class="left-logo-box"></div>
</div>
<!-- 登录 -->
<div class="right" v-if="dataStore.type === 1">
<div class="right-logo-box">
<div>
<div class="right-logo-font">登录</div>
<div class="sign-in">Sign in</div>
</div>
</div>
<div style="margin-left: 100px">
<el-form
ref="ruleFormRef"
style="width: 300px"
:model="dataStore.ruleForm"
:rules="dataStore.rules"
label-width="auto"
label-position="top"
class="demo-ruleForm"
hide-required-asterisk
>
<el-form-item label="账号" prop="username">
<el-input v-model="dataStore.ruleForm.username" :prefix-icon="Iphone" autocomplete="off" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="dataStore.ruleForm.password"
type="password"
show-password
:prefix-icon="Lock"
autocomplete="off"
/>
<!-- <div
style="
width: 350px;
font-size: 12px;
line-height: 1;
color: #4178d5;
text-align: right;
cursor: pointer;
"
@click="handleTabClick(2)"
>
忘记密码?
</div> -->
</el-form-item>
<!-- <el-form-item prop="captcha">
<span style="margin-right: 10px; color: #606266">验证码</span>
<el-input v-model="dataStore.ruleForm.captcha" style="width: 80px" autocomplete="off" />
<img :src="dataStore.base64" style="width: 150px; height: 60px; margin: 0 6px" />
<el-button type="primary" v-debounce="getLoginCodeImg">刷新</el-button>
</el-form-item> -->
<el-form-item>
<el-button
size="large"
type="primary"
style="width: 100px; height: 36px; border-radius: 2px; box-shadow: 2px 5px 16px #4178d5"
v-debounce="submitForm"
>登录</el-button
>
</el-form-item>
</el-form>
</div>
</div>
<!--忘记密码 -->
<div class="right" v-if="dataStore.type === 2">
<div style="margin-left: 100px">
<el-button
type="primary"
:icon="ArrowLeftBold"
circle
style="margin: 60px 0 20px"
@click="handleTabClick(1)"
/>
<el-form
ref="resetRuleFormRef"
:model="dataStore.resetRuleForm"
:rules="dataStore.resetRules"
label-width="auto"
label-position="top"
class="demo-ruleForm"
hide-required-asterisk
>
<el-form-item label="手机号" prop="iphone">
<el-input v-model="dataStore.resetRuleForm.iphone" :prefix-icon="Iphone" autocomplete="off" />
</el-form-item>
<el-form-item prop="code" label="验证码">
<el-input v-model="dataStore.resetRuleForm.code" autocomplete="off">
<template #append>
<div style="font-size: 14px; color: #4178d5; cursor: pointer" v-debounce="handleGetCode">
{{ dataStore.codeFont }}
</div>
<div v-if="dataStore.isShowTime" style="margin: 0 5px; font-size: 14px; color: #4178d5">
{{ dataStore.timeCount }}s
</div>
</template>
</el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="dataStore.resetRuleForm.newPassword"
type="password"
show-password
:prefix-icon="Lock"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="dataStore.resetRuleForm.confirmPassword"
type="password"
show-password
:prefix-icon="Lock"
autocomplete="off"
/>
</el-form-item>
<el-form-item>
<el-button
size="large"
type="primary"
style="width: 100px; height: 36px; border-radius: 2px; box-shadow: 2px 5px 16px #4178d5"
v-debounce="resetConfirmForm"
>提交</el-button
>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import CryptoJS from "crypto-js";
//loginCodeImgApi
import { loginApi } from "@/api/modules/login";
import { Iphone, Lock, ArrowLeftBold } from "@element-plus/icons-vue";
import { RULES, RULE_FORM, RESET_RULES, RESET_RULE_FORM } from "./constant/index";
import { cloneDeep } from "lodash-es";
//用户信息存储
import { useUserStore } from "@/stores/modules/user";
const userStore = useUserStore();
const $router = useRouter();
//登录表单Ref
const ruleFormRef: any = ref(null);
//修改密码表单Ref
const resetRuleFormRef: any = ref(null);
//数据源
const dataStore = reactive<any>({
timeCount: 120, //修改密码里验证码倒计时
isShowTime: false,
base64: "", //验证码图片
type: 1, //1:登录,2:修改密码
codeFont: "获取验证码", //修改密码里的验证码按钮文字
ruleForm: cloneDeep(RULE_FORM), //登录表单数据
rules: cloneDeep(RULES), //登录表单验证
resetRuleForm: cloneDeep(RESET_RULE_FORM), //修改密码表单数据
resetRules: cloneDeep(RESET_RULES) //修改密码表单验证
});
//计时器
let intervalId: any = null;
//登录
const submitForm = () => {
console.log(ruleFormRef);
if (!ruleFormRef) return;
ruleFormRef?.value?.validate((valid: any) => {
if (valid) {
login();
} else {
console.log("error submit!");
}
});
};
//忘记密码提交事件
const resetConfirmForm = () => {
if (!resetRuleFormRef) return;
resetRuleFormRef?.value?.validate((valid: any) => {
if (valid) {
let password = CryptoJS.MD5(dataStore.ruleForm.password)?.toString();
console.log(password, "==== dataStore.ruleForm.password====");
console.log("submit!");
} else {
console.log("error submit!");
}
});
};
//登录接口
const login = async () => {
const result: any = await loginApi({
...dataStore.ruleForm,
password: CryptoJS?.MD5(dataStore.ruleForm.password)?.toString()
});
if (result?.code === 0) {
const { data } = result;
setUserData(data);
}
// else {
// getLoginCodeImg();
// }
};
//获取验证码图片接口
// const getLoginCodeImg = async () => {
// const result: any = await loginCodeImgApi();
// if (result?.code === 0) {
// const { data } = result;
// const { captcha, token } = data;
// dataStore.base64 = captcha;
// dataStore.ruleForm.token = token;
// }
// };
// getLoginCodeImg();
//登录和密码重置切换
const handleTabClick = (type: any) => {
dataStore.type = type;
for (let key in dataStore.ruleForm) {
dataStore.ruleForm[key] = "";
}
for (let key in dataStore.resetRuleForm) {
dataStore.resetRuleForm[key] = "";
}
if (intervalId) {
handleClearInterval();
dataStore.timeCount = 120;
dataStore.isShowTime = false;
}
};
//120秒验证时间
const updateCountdown = () => {
intervalId = setInterval(() => {
if (dataStore.timeCount > 0) {
dataStore.isShowTime = true;
dataStore.timeCount--;
} else {
handleClearInterval();
dataStore.timeCount = 120;
dataStore.isShowTime = false;
}
}, 1000);
};
//获取验证码
const handleGetCode = () => {
//如果计时器已经启动了,就不要再去触发了
if (intervalId) {
return;
}
resetRuleFormRef.value.validateField("iphone", (valid: any) => {
if (valid) {
updateCountdown();
} else {
}
});
};
//清理定时器
const handleClearInterval = () => {
clearInterval(intervalId);
intervalId = null;
};
// 设置用户数据
const setUserData = (data: any) => {
const { username, uid, token, avatar } = data;
console.log();
// 设置token
userStore.setToken("Bearer" + " " + token);
userStore.setUid(uid);
userStore.setNickname(username);
userStore.setAvatar(avatar);
// await userStore.getAuthMenuList();
//跳转到首页
setTimeout(() => {
$router.push("/admin/index");
}, 500);
};
onUnmounted(() => {
//页面卸载的时候清空定时器
if (intervalId) {
handleClearInterval();
}
});
</script>
<style lang="scss">
@import "./index.scss";
</style>

View File

@@ -38,7 +38,7 @@
<!-- <el-input v-model="_ruleFormParam.sort" /> --> <!-- <el-input v-model="_ruleFormParam.sort" /> -->
<el-input-number <el-input-number
:min="1" :min="0"
:max="9999" :max="9999"
:controls="true" :controls="true"
v-model="_ruleFormParam.sort" v-model="_ruleFormParam.sort"

View File

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

View File

@@ -35,27 +35,10 @@ export const FORM_DATA: FormItem[] = [
prop: "treeIds", prop: "treeIds",
prop1: "category_id", prop1: "category_id",
placeholder: "请选择", placeholder: "请选择",
type: "treeSelect", type: "treeSelect", //treeSelect
isArray: true, isArray: true,
label: "产品分类: ", label: "产品分类: ",
options: [ 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"
}
]
}
]
}
]
}, },
{ {
prop: "Time", prop: "Time",

View File

@@ -3,7 +3,7 @@
<div class="table-box"> <div class="table-box">
<div style="padding-bottom: 16px"> <div style="padding-bottom: 16px">
<el-button @click="handleReset(dataStore)"> 重置 </el-button> <el-button @click="handleReset(dataStore, editorRef)"> 重置 </el-button>
<el-button type="primary" @click="handleSubmit(infoRef, imgInfoRef, dataStore)"> 提交 </el-button> <el-button type="primary" @click="handleSubmit(infoRef, imgInfoRef, dataStore)"> 提交 </el-button>
</div> </div>
<div class="card table-main"> <div class="card table-main">
@@ -16,7 +16,7 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="产品详情" name="third"> <el-tab-pane label="产品详情" name="third">
<div style="width: 1280px; margin: 0 auto"> <div style="width: 1280px; margin: 0 auto">
<Editor v-model:content="dataStore.detail" /> <Editor v-model:content="dataStore.detail" ref="editorRef" />
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="相关信息" name="related"> <el-tab-pane label="相关信息" name="related">
@@ -65,6 +65,9 @@ import imgInfo from "./components/imgInfo.vue";
import FormTable from "@/components/FormTable/index.vue"; import FormTable from "@/components/FormTable/index.vue";
const $route = useRoute(); const $route = useRoute();
const editorRef = ref<any>(null);
//数据集合 //数据集合
const dataStore = reactive<any>({ const dataStore = reactive<any>({
relatedColumns: cloneDeep(RELATED_INFO_COLUMNS), //相关信息及下载表格配置 relatedColumns: cloneDeep(RELATED_INFO_COLUMNS), //相关信息及下载表格配置
@@ -94,7 +97,7 @@ const getProductDetails = async () => {
if (result?.code === 0) { if (result?.code === 0) {
const { data } = result; const { data } = result;
//参数初始化(将参数按照不同的tab区分出来) //参数初始化(将参数按照不同的tab区分出来)
initDetailParams(dataStore, data); initDetailParams(dataStore, data, editorRef);
} }
}; };
getProductDetails(); getProductDetails();

View File

@@ -15,8 +15,37 @@ const hasIdRecursive = (data: any, targetId: any) => {
return false; 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) => { export const initDetailParams = (dataStore: any, data: any, editorRef: any) => {
let is = hasIdRecursive(dataStore.options, data.category_id); let is = hasIdRecursive(dataStore.options, data.category_id);
//基本信息 //基本信息
dataStore.basicInfoRuleForm = cloneDeep({ dataStore.basicInfoRuleForm = cloneDeep({
@@ -40,9 +69,15 @@ export const initDetailParams = (dataStore: any, data: any) => {
stock_qty: data.stock_qty, stock_qty: data.stock_qty,
id: data.id id: data.id
}); });
//详情 //详情
dataStore.detail = cloneDeep(data.detail); if (!data.detail) {
//dataStore.detail = convertSpanToDiv(dataStore.detail); dataStore.detail = "";
editorRef?.value?.clearEditor(); // 调用子组件的清空方法
} else {
dataStore.detail = htmlDecode(data.detail); //htmlDecode(data.detail);
}
//图片 //图片
dataStore.imgInfoData.cover_image = data.cover_image; dataStore.imgInfoData.cover_image = data.cover_image;
dataStore.imgInfoData.video_url = data.video_url; dataStore.imgInfoData.video_url = data.video_url;

View File

@@ -4,19 +4,19 @@ import { messageBox } from "@/utils/messageBox";
// import { cloneDeep } from "lodash-es"; // import { cloneDeep } from "lodash-es";
import { initDetailParams } from "./initDetailParams"; import { initDetailParams } from "./initDetailParams";
//详情(重置,重新获取一下详情) //详情(重置,重新获取一下详情)
const getProductDetails = async (dataStore: any) => { const getProductDetails = async (dataStore: any, editorRef: any) => {
const { id } = dataStore.basicInfoRuleForm; const { id } = dataStore.basicInfoRuleForm;
const result = await getProductDetailsApi(id); const result = await getProductDetailsApi(id);
if (result?.code === 0) { if (result?.code === 0) {
const { data } = result; const { data } = result;
initDetailParams(dataStore, data); initDetailParams(dataStore, data, editorRef);
useMsg("success", "重置成功 !"); useMsg("success", "重置成功 !");
} }
}; };
export const handleReset = (dataStore: any) => { export const handleReset = (dataStore: any, editorRef?: any) => {
messageBox("该操作会将数据重置为初始状态", () => { messageBox("该操作会将数据重置为初始状态", () => {
getProductDetails(dataStore); getProductDetails(dataStore, editorRef);
}); });
}; };

View File

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

File diff suppressed because one or more lines are too long