优化仪表盘内容
This commit is contained in:
144
src/components/scEditor/UploadAdapter.js
Normal file
144
src/components/scEditor/UploadAdapter.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
export default class UploadAdapter {
|
||||||
|
constructor(loader, options) {
|
||||||
|
this.loader = loader;
|
||||||
|
this.options = options;
|
||||||
|
this.timeout = 60000; // 60秒超时
|
||||||
|
}
|
||||||
|
|
||||||
|
upload() {
|
||||||
|
return this.loader.file.then(
|
||||||
|
(file) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
this._initRequest();
|
||||||
|
this._initListeners(resolve, reject, file);
|
||||||
|
this._sendRequest(file);
|
||||||
|
this._initTimeout(reject);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
if (this.xhr) {
|
||||||
|
this.xhr.abort();
|
||||||
|
}
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_initRequest() {
|
||||||
|
const xhr = (this.xhr = new XMLHttpRequest());
|
||||||
|
|
||||||
|
xhr.open("POST", this.options.upload.uploadUrl, true);
|
||||||
|
xhr.responseType = "json";
|
||||||
|
}
|
||||||
|
|
||||||
|
_initListeners(resolve, reject, file) {
|
||||||
|
const xhr = this.xhr;
|
||||||
|
const loader = this.loader;
|
||||||
|
const genericErrorText = `Couldn't upload file: ${file.name}.`;
|
||||||
|
|
||||||
|
xhr.addEventListener("error", () => {
|
||||||
|
console.error("[UploadAdapter] Upload error for file:", file.name);
|
||||||
|
reject(genericErrorText);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener("abort", () => {
|
||||||
|
console.warn("[UploadAdapter] Upload aborted for file:", file.name);
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener("timeout", () => {
|
||||||
|
console.error("[UploadAdapter] Upload timeout for file:", file.name);
|
||||||
|
reject(`Upload timeout: ${file.name}. Please try again.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener("load", () => {
|
||||||
|
const response = xhr.response;
|
||||||
|
|
||||||
|
// 检查响应状态码
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
if (!response) {
|
||||||
|
console.error("[UploadAdapter] Empty response for file:", file.name);
|
||||||
|
reject(genericErrorText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查业务状态码(假设 code=1 表示成功)
|
||||||
|
if (response.code == 1 || response.code == undefined) {
|
||||||
|
const url = response.data?.url || response.data?.src;
|
||||||
|
if (!url) {
|
||||||
|
console.error("[UploadAdapter] No URL in response for file:", file.name, response);
|
||||||
|
reject("Upload succeeded but no URL returned");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({ default: url });
|
||||||
|
} else {
|
||||||
|
const errorMessage = response.message || genericErrorText;
|
||||||
|
console.error("[UploadAdapter] Upload failed for file:", file.name, "Error:", errorMessage);
|
||||||
|
reject(errorMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("[UploadAdapter] HTTP error for file:", file.name, "Status:", xhr.status);
|
||||||
|
reject(`Server error (${xhr.status}): ${file.name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 上传进度监听
|
||||||
|
if (xhr.upload) {
|
||||||
|
xhr.upload.addEventListener("progress", (evt) => {
|
||||||
|
if (evt.lengthComputable) {
|
||||||
|
loader.uploadTotal = evt.total;
|
||||||
|
loader.uploaded = evt.loaded;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_initTimeout(reject) {
|
||||||
|
// 清除之前的超时定时器(如果有)
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的超时定时器
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
if (this.xhr) {
|
||||||
|
this.xhr.abort();
|
||||||
|
reject(new Error("Upload timeout"));
|
||||||
|
}
|
||||||
|
}, this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendRequest(file) {
|
||||||
|
// 设置请求超时
|
||||||
|
this.xhr.timeout = this.timeout;
|
||||||
|
|
||||||
|
// Set headers if specified.
|
||||||
|
const headers = this.options.upload.headers || {};
|
||||||
|
const extendData = this.options.upload.extendData || {};
|
||||||
|
// Use the withCredentials flag if specified.
|
||||||
|
const withCredentials = this.options.upload.withCredentials || false;
|
||||||
|
const uploadName = this.options.upload.uploadName || "file";
|
||||||
|
|
||||||
|
for (const headerName of Object.keys(headers)) {
|
||||||
|
this.xhr.setRequestHeader(headerName, headers[headerName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xhr.withCredentials = withCredentials;
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
for (const key of Object.keys(extendData)) {
|
||||||
|
data.append(key, extendData[key]);
|
||||||
|
}
|
||||||
|
data.append(uploadName, file);
|
||||||
|
|
||||||
|
this.xhr.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UploadAdapterPlugin(editor) {
|
||||||
|
editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
|
||||||
|
return new UploadAdapter(loader, editor.config._config);
|
||||||
|
};
|
||||||
|
}
|
||||||
389
src/components/scEditor/index.vue
Normal file
389
src/components/scEditor/index.vue
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
<template>
|
||||||
|
<div :style="{ '--editor-height': editorHeight }">
|
||||||
|
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur"
|
||||||
|
@focus="onFocus"></ckeditor>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ClassicEditor,
|
||||||
|
Alignment,
|
||||||
|
AutoImage,
|
||||||
|
Autoformat,
|
||||||
|
BlockQuote,
|
||||||
|
Bold,
|
||||||
|
CodeBlock,
|
||||||
|
DataFilter,
|
||||||
|
DataSchema,
|
||||||
|
Essentials,
|
||||||
|
FindAndReplace,
|
||||||
|
FontBackgroundColor,
|
||||||
|
FontColor,
|
||||||
|
FontFamily,
|
||||||
|
FontSize,
|
||||||
|
GeneralHtmlSupport,
|
||||||
|
Heading,
|
||||||
|
Highlight,
|
||||||
|
HorizontalLine,
|
||||||
|
Image,
|
||||||
|
ImageCaption,
|
||||||
|
ImageInsert,
|
||||||
|
ImageResize,
|
||||||
|
ImageStyle,
|
||||||
|
ImageToolbar,
|
||||||
|
ImageUpload,
|
||||||
|
Indent,
|
||||||
|
IndentBlock,
|
||||||
|
Italic,
|
||||||
|
Link,
|
||||||
|
LinkImage,
|
||||||
|
List,
|
||||||
|
MediaEmbed,
|
||||||
|
MediaEmbedToolbar,
|
||||||
|
Mention,
|
||||||
|
Paragraph,
|
||||||
|
PasteFromOffice,
|
||||||
|
RemoveFormat,
|
||||||
|
SelectAll,
|
||||||
|
ShowBlocks,
|
||||||
|
SourceEditing,
|
||||||
|
SpecialCharacters,
|
||||||
|
SpecialCharactersArrows,
|
||||||
|
SpecialCharactersCurrency,
|
||||||
|
SpecialCharactersEssentials,
|
||||||
|
SpecialCharactersLatin,
|
||||||
|
SpecialCharactersMathematical,
|
||||||
|
SpecialCharactersText,
|
||||||
|
Style,
|
||||||
|
Subscript,
|
||||||
|
Superscript,
|
||||||
|
Table,
|
||||||
|
TableCaption,
|
||||||
|
TableCellProperties,
|
||||||
|
TableColumnResize,
|
||||||
|
TableProperties,
|
||||||
|
TableToolbar,
|
||||||
|
TextTransformation,
|
||||||
|
TodoList,
|
||||||
|
Underline,
|
||||||
|
Undo,
|
||||||
|
WordCount,
|
||||||
|
} from "ckeditor5";
|
||||||
|
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
|
||||||
|
import { UploadAdapterPlugin } from "./UploadAdapter.js";
|
||||||
|
|
||||||
|
import { ref, computed, watch } from "vue";
|
||||||
|
import { useCurrentInstance } from "@/utils/tool";
|
||||||
|
|
||||||
|
import coreTranslations from "ckeditor5/translations/zh-cn.js";
|
||||||
|
import "ckeditor5/ckeditor5.css";
|
||||||
|
|
||||||
|
const { proxy } = useCurrentInstance();
|
||||||
|
|
||||||
|
// 组件名称
|
||||||
|
defineOptions({
|
||||||
|
name: "scCkeditor"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Props 定义
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "请输入内容……",
|
||||||
|
},
|
||||||
|
toolbar: {
|
||||||
|
type: String,
|
||||||
|
default: "basic",
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: "400px",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits 定义
|
||||||
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
// 工具栏配置常量
|
||||||
|
const TOOLBARS = {
|
||||||
|
full: [
|
||||||
|
"sourceEditing",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
"heading",
|
||||||
|
"style",
|
||||||
|
"|",
|
||||||
|
"superscript",
|
||||||
|
"subscript",
|
||||||
|
"removeFormat",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"underline",
|
||||||
|
"link",
|
||||||
|
"fontBackgroundColor",
|
||||||
|
"fontFamily",
|
||||||
|
"fontSize",
|
||||||
|
"fontColor",
|
||||||
|
"|",
|
||||||
|
"outdent",
|
||||||
|
"indent",
|
||||||
|
"alignment",
|
||||||
|
"bulletedList",
|
||||||
|
"numberedList",
|
||||||
|
"todoList",
|
||||||
|
"|",
|
||||||
|
"blockQuote",
|
||||||
|
"insertTable",
|
||||||
|
"imageInsert",
|
||||||
|
"mediaEmbed",
|
||||||
|
"highlight",
|
||||||
|
"horizontalLine",
|
||||||
|
"selectAll",
|
||||||
|
"showBlocks",
|
||||||
|
"specialCharacters",
|
||||||
|
"codeBlock",
|
||||||
|
"findAndReplace",
|
||||||
|
],
|
||||||
|
basic: [
|
||||||
|
"sourceEditing",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
"heading",
|
||||||
|
"|",
|
||||||
|
"removeFormat",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"underline",
|
||||||
|
"link",
|
||||||
|
"fontBackgroundColor",
|
||||||
|
"fontFamily",
|
||||||
|
"fontSize",
|
||||||
|
"fontColor",
|
||||||
|
"|",
|
||||||
|
"outdent",
|
||||||
|
"indent",
|
||||||
|
"alignment",
|
||||||
|
"bulletedList",
|
||||||
|
"numberedList",
|
||||||
|
"todoList",
|
||||||
|
"|",
|
||||||
|
"insertTable",
|
||||||
|
"imageInsert",
|
||||||
|
"mediaEmbed",
|
||||||
|
],
|
||||||
|
simple: [
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
"heading",
|
||||||
|
"|",
|
||||||
|
"removeFormat",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"underline",
|
||||||
|
"link",
|
||||||
|
"fontBackgroundColor",
|
||||||
|
"fontFamily",
|
||||||
|
"fontSize",
|
||||||
|
"fontColor",
|
||||||
|
"|",
|
||||||
|
"insertTable",
|
||||||
|
"imageInsert",
|
||||||
|
"mediaEmbed",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 插件配置常量
|
||||||
|
const PLUGINS = [
|
||||||
|
Alignment,
|
||||||
|
AutoImage,
|
||||||
|
Autoformat,
|
||||||
|
BlockQuote,
|
||||||
|
Bold,
|
||||||
|
CodeBlock,
|
||||||
|
DataFilter,
|
||||||
|
DataSchema,
|
||||||
|
Essentials,
|
||||||
|
FindAndReplace,
|
||||||
|
FontBackgroundColor,
|
||||||
|
FontColor,
|
||||||
|
FontFamily,
|
||||||
|
FontSize,
|
||||||
|
GeneralHtmlSupport,
|
||||||
|
Heading,
|
||||||
|
Highlight,
|
||||||
|
HorizontalLine,
|
||||||
|
Image,
|
||||||
|
ImageCaption,
|
||||||
|
ImageInsert,
|
||||||
|
ImageResize,
|
||||||
|
ImageStyle,
|
||||||
|
ImageToolbar,
|
||||||
|
ImageUpload,
|
||||||
|
Indent,
|
||||||
|
IndentBlock,
|
||||||
|
Italic,
|
||||||
|
Link,
|
||||||
|
LinkImage,
|
||||||
|
List,
|
||||||
|
MediaEmbed,
|
||||||
|
MediaEmbedToolbar,
|
||||||
|
Mention,
|
||||||
|
Paragraph,
|
||||||
|
PasteFromOffice,
|
||||||
|
RemoveFormat,
|
||||||
|
SelectAll,
|
||||||
|
ShowBlocks,
|
||||||
|
SourceEditing,
|
||||||
|
SpecialCharacters,
|
||||||
|
SpecialCharactersArrows,
|
||||||
|
SpecialCharactersCurrency,
|
||||||
|
SpecialCharactersEssentials,
|
||||||
|
SpecialCharactersLatin,
|
||||||
|
SpecialCharactersMathematical,
|
||||||
|
SpecialCharactersText,
|
||||||
|
Style,
|
||||||
|
Subscript,
|
||||||
|
Superscript,
|
||||||
|
Table,
|
||||||
|
TableCaption,
|
||||||
|
TableCellProperties,
|
||||||
|
TableColumnResize,
|
||||||
|
TableProperties,
|
||||||
|
TableToolbar,
|
||||||
|
TextTransformation,
|
||||||
|
TodoList,
|
||||||
|
Underline,
|
||||||
|
Undo,
|
||||||
|
WordCount,
|
||||||
|
UploadAdapterPlugin,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const editorData = ref("");
|
||||||
|
const editorHeight = ref(props.height);
|
||||||
|
const editor = ClassicEditor;
|
||||||
|
|
||||||
|
// 编辑器配置
|
||||||
|
const editorConfig = computed(() => ({
|
||||||
|
language: { ui: "zh-cn", content: "zh-cn" },
|
||||||
|
translations: [coreTranslations],
|
||||||
|
plugins: PLUGINS,
|
||||||
|
toolbar: {
|
||||||
|
shouldNotGroupWhenFull: true,
|
||||||
|
items: TOOLBARS[props.toolbar] || TOOLBARS.basic,
|
||||||
|
},
|
||||||
|
placeholder: props.placeholder,
|
||||||
|
image: {
|
||||||
|
styles: ["alignLeft", "alignCenter", "alignRight"],
|
||||||
|
toolbar: [
|
||||||
|
"imageTextAlternative",
|
||||||
|
"toggleImageCaption",
|
||||||
|
"|",
|
||||||
|
"imageStyle:alignLeft",
|
||||||
|
"imageStyle:alignCenter",
|
||||||
|
"imageStyle:alignRight",
|
||||||
|
"|",
|
||||||
|
"linkImage",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
mediaEmbed: {
|
||||||
|
previewsInData: true,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
name: "mp4",
|
||||||
|
url: /\.(mp4|avi|mov|flv|wmv|mkv)$/i,
|
||||||
|
html: match => {
|
||||||
|
const url = match["input"];
|
||||||
|
return ('<video controls width="100%" height="100%" src="' + url + '"></video>')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
options: [10, 12, 14, 16, 18, 20, 22, 24, 26, 30, 32, 36],
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
definitions: [
|
||||||
|
{
|
||||||
|
name: "Article category",
|
||||||
|
element: "h3",
|
||||||
|
classes: ["category"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Info box",
|
||||||
|
element: "p",
|
||||||
|
classes: ["info-box"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
uploadUrl: proxy?.$API?.common?.upload?.url || "",
|
||||||
|
withCredentials: false,
|
||||||
|
extendData: { type: "images" },
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + proxy?.$TOOL?.data?.get("TOKEN"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 监听 modelValue 变化
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newVal) => {
|
||||||
|
editorData.value = newVal ?? "";
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听 height 变化
|
||||||
|
watch(
|
||||||
|
() => props.height,
|
||||||
|
(newVal) => {
|
||||||
|
editorHeight.value = newVal;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 移除图片宽高的正则替换函数
|
||||||
|
const stripImageDimensions = (html) => {
|
||||||
|
return html.replace(/<img[^>]*>/gi, (match) => {
|
||||||
|
return match
|
||||||
|
.replace(/width="[^"]*"/gi, "")
|
||||||
|
.replace(/height="[^"]*"/gi, "");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 失去焦点事件 - 移除图片的固定宽高,避免响应式布局问题
|
||||||
|
const onBlur = () => {
|
||||||
|
const cleanedData = stripImageDimensions(editorData.value);
|
||||||
|
editorData.value = cleanedData;
|
||||||
|
emit("update:modelValue", cleanedData);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ck-z-panel: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-content {
|
||||||
|
height: var(--editor-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-source-editing-area,
|
||||||
|
.ck-source-editing-area textarea {
|
||||||
|
height: var(--editor-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-source-editing-area textarea {
|
||||||
|
overflow-y: scroll !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,6 +30,14 @@ export default {
|
|||||||
//语言
|
//语言
|
||||||
LANG: 'zh-cn',
|
LANG: 'zh-cn',
|
||||||
|
|
||||||
|
DASHBOARD_LAYOUT: 'widgets', //控制台首页默认布局
|
||||||
|
DEFAULT_GRID: {
|
||||||
|
//默认分栏数量和宽度 例如 [24] [18,6] [8,8,8] [6,12,6]
|
||||||
|
layout: [24, 12, 12],
|
||||||
|
//小组件分布,com取值:pages/home/components 文件名
|
||||||
|
compsList: [["welcome"], ["info"], ["ver"]],
|
||||||
|
},
|
||||||
|
|
||||||
//是否加密localStorage, 为空不加密
|
//是否加密localStorage, 为空不加密
|
||||||
//支持多种加密方式: 'AES', 'BASE64', 'DES'
|
//支持多种加密方式: 'AES', 'BASE64', 'DES'
|
||||||
LS_ENCRYPTION: '',
|
LS_ENCRYPTION: '',
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed, defineAsyncComponent } from 'vue'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import config from '@/config'
|
||||||
|
|
||||||
// 定义组件名称
|
// 定义组件名称
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -15,7 +15,7 @@ defineOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const dashboard = ref(import.meta.env.VITE_APP_DASHBOARD || 'work')
|
const dashboard = ref(config.DASHBOARD_LAYOUT || 'work')
|
||||||
|
|
||||||
// 动态导入组件
|
// 动态导入组件
|
||||||
const components = {
|
const components = {
|
||||||
|
|||||||
@@ -5,11 +5,15 @@
|
|||||||
<div class="widgets-top-title">控制台</div>
|
<div class="widgets-top-title">控制台</div>
|
||||||
<div class="widgets-top-actions">
|
<div class="widgets-top-actions">
|
||||||
<a-button v-if="customizing" type="primary" shape="round" @click="handleSave">
|
<a-button v-if="customizing" type="primary" shape="round" @click="handleSave">
|
||||||
<template #icon><CheckOutlined /></template>
|
<template #icon>
|
||||||
|
<CheckOutlined />
|
||||||
|
</template>
|
||||||
完成
|
完成
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button v-else type="primary" shape="round" @click="handleCustom">
|
<a-button v-else type="primary" shape="round" @click="handleCustom">
|
||||||
<template #icon><EditOutlined /></template>
|
<template #icon>
|
||||||
|
<EditOutlined />
|
||||||
|
</template>
|
||||||
自定义
|
自定义
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,33 +24,19 @@
|
|||||||
<a-empty description="没有部件啦" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
<a-empty description="没有部件啦" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
||||||
</div>
|
</div>
|
||||||
<a-row :gutter="15">
|
<a-row :gutter="15">
|
||||||
<a-col
|
<a-col v-for="(item, index) in grid.layout" :key="index" :md="item" :xs="24">
|
||||||
v-for="(item, index) in grid.layout"
|
|
||||||
:key="index"
|
|
||||||
:md="item"
|
|
||||||
:xs="24"
|
|
||||||
>
|
|
||||||
<div class="draggable-wrapper">
|
<div class="draggable-wrapper">
|
||||||
<draggable
|
<draggable v-model="grid.compsList[index]" item-key="key" :animation="200"
|
||||||
v-model="grid.compsList[index]"
|
handle=".customize-overlay" group="widgets" class="draggable-box">
|
||||||
item-key="key"
|
|
||||||
:animation="200"
|
|
||||||
handle=".customize-overlay"
|
|
||||||
group="widgets"
|
|
||||||
class="draggable-box"
|
|
||||||
>
|
|
||||||
<template #item="{ element }">
|
<template #item="{ element }">
|
||||||
<div class="widgets-item">
|
<div class="widgets-item">
|
||||||
<component :is="allComps[element]" />
|
<component :is="allComps[element]" />
|
||||||
<div v-if="customizing" class="customize-overlay">
|
<div v-if="customizing" class="customize-overlay">
|
||||||
<a-button
|
<a-button class="close" type="primary" ghost shape="circle"
|
||||||
class="close"
|
@click="removeComp(element)">
|
||||||
type="primary"
|
<template #icon>
|
||||||
ghost
|
<CloseOutlined />
|
||||||
shape="circle"
|
</template>
|
||||||
@click="removeComp(element)"
|
|
||||||
>
|
|
||||||
<template #icon><CloseOutlined /></template>
|
|
||||||
</a-button>
|
</a-button>
|
||||||
<label>
|
<label>
|
||||||
<component :is="allComps[element].icon" />
|
<component :is="allComps[element].icon" />
|
||||||
@@ -64,15 +54,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 自定义侧边栏 -->
|
<!-- 自定义侧边栏 -->
|
||||||
<a-drawer
|
<a-drawer v-if="customizing" :open="customizing" :width="360" placement="right" :closable="false" :mask="false"
|
||||||
v-if="customizing"
|
class="widgets-drawer">
|
||||||
:open="customizing"
|
|
||||||
:width="360"
|
|
||||||
placement="right"
|
|
||||||
:closable="false"
|
|
||||||
:mask="false"
|
|
||||||
class="widgets-drawer"
|
|
||||||
>
|
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="widgets-aside-title">
|
<div class="widgets-aside-title">
|
||||||
<PlusCircleOutlined /> 添加部件
|
<PlusCircleOutlined /> 添加部件
|
||||||
@@ -81,7 +64,9 @@
|
|||||||
|
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button type="text" @click="handleClose">
|
<a-button type="text" @click="handleClose">
|
||||||
<template #icon><CloseOutlined /></template>
|
<template #icon>
|
||||||
|
<CloseOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -89,33 +74,24 @@
|
|||||||
<div class="select-layout">
|
<div class="select-layout">
|
||||||
<h3>选择布局</h3>
|
<h3>选择布局</h3>
|
||||||
<div class="select-layout-options">
|
<div class="select-layout-options">
|
||||||
<div
|
<div class="select-layout-item item01" :class="{ active: grid.layout.join(',') === '12,6,6' }"
|
||||||
class="select-layout-item item01"
|
@click="setLayout([12, 6, 6])">
|
||||||
:class="{ active: grid.layout.join(',') === '12,6,6' }"
|
|
||||||
@click="setLayout([12, 6, 6])"
|
|
||||||
>
|
|
||||||
<a-row :gutter="2">
|
<a-row :gutter="2">
|
||||||
<a-col :span="12"><span></span></a-col>
|
<a-col :span="12"><span></span></a-col>
|
||||||
<a-col :span="6"><span></span></a-col>
|
<a-col :span="6"><span></span></a-col>
|
||||||
<a-col :span="6"><span></span></a-col>
|
<a-col :span="6"><span></span></a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="select-layout-item item02" :class="{ active: grid.layout.join(',') === '24,16,8' }"
|
||||||
class="select-layout-item item02"
|
@click="setLayout([24, 16, 8])">
|
||||||
:class="{ active: grid.layout.join(',') === '24,16,8' }"
|
|
||||||
@click="setLayout([24, 16, 8])"
|
|
||||||
>
|
|
||||||
<a-row :gutter="2">
|
<a-row :gutter="2">
|
||||||
<a-col :span="24"><span></span></a-col>
|
<a-col :span="24"><span></span></a-col>
|
||||||
<a-col :span="16"><span></span></a-col>
|
<a-col :span="16"><span></span></a-col>
|
||||||
<a-col :span="8"><span></span></a-col>
|
<a-col :span="8"><span></span></a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="select-layout-item item03" :class="{ active: grid.layout.join(',') === '24' }"
|
||||||
class="select-layout-item item03"
|
@click="setLayout([24])">
|
||||||
:class="{ active: grid.layout.join(',') === '24' }"
|
|
||||||
@click="setLayout([24])"
|
|
||||||
>
|
|
||||||
<a-row :gutter="2">
|
<a-row :gutter="2">
|
||||||
<a-col :span="24"><span></span></a-col>
|
<a-col :span="24"><span></span></a-col>
|
||||||
<a-col :span="24"><span></span></a-col>
|
<a-col :span="24"><span></span></a-col>
|
||||||
@@ -141,7 +117,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<a-button type="primary" @click="addComp(item)">
|
<a-button type="primary" @click="addComp(item)">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,90 +136,17 @@
|
|||||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||||
import { Empty } from 'ant-design-vue'
|
import { Empty } from 'ant-design-vue'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import {
|
|
||||||
CheckOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
PlusCircleOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import allComps from './components'
|
import allComps from './components'
|
||||||
|
import config from '@/config'
|
||||||
|
|
||||||
// 定义组件名称
|
// 定义组件名称
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'WidgetsPage',
|
name: 'WidgetsPage',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义组件元数据
|
|
||||||
allComps.welcome.icon = 'GiftOutlined'
|
|
||||||
allComps.welcome.title = '欢迎'
|
|
||||||
allComps.welcome.description = '项目特色以及文档链接'
|
|
||||||
|
|
||||||
allComps.info.icon = 'MonitorOutlined'
|
|
||||||
allComps.info.title = '系统信息'
|
|
||||||
allComps.info.description = '当前项目系统信息'
|
|
||||||
|
|
||||||
allComps.about.icon = 'SettingOutlined'
|
|
||||||
allComps.about.title = '关于项目'
|
|
||||||
allComps.about.description = '点个星星支持一下'
|
|
||||||
|
|
||||||
allComps.echarts.icon = 'LineChartOutlined'
|
|
||||||
allComps.echarts.title = '实时收入'
|
|
||||||
allComps.echarts.description = 'Echarts组件演示'
|
|
||||||
|
|
||||||
allComps.progress.icon = 'DashboardOutlined'
|
|
||||||
allComps.progress.title = '进度环'
|
|
||||||
allComps.progress.description = '进度环原子组件演示'
|
|
||||||
|
|
||||||
allComps.time.icon = 'ClockCircleOutlined'
|
|
||||||
allComps.time.title = '时钟'
|
|
||||||
allComps.time.description = '演示部件效果'
|
|
||||||
|
|
||||||
allComps.sms.icon = 'MessageOutlined'
|
|
||||||
allComps.sms.title = '短信统计'
|
|
||||||
allComps.sms.description = '短信统计'
|
|
||||||
|
|
||||||
allComps.ver.icon = 'FileTextOutlined'
|
|
||||||
allComps.ver.title = '版本信息'
|
|
||||||
allComps.ver.description = '当前项目版本信息'
|
|
||||||
|
|
||||||
// 导入图标组件
|
|
||||||
import {
|
|
||||||
GiftOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
SettingOutlined as SettingIcon,
|
|
||||||
LineChartOutlined,
|
|
||||||
DashboardOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
|
|
||||||
// 图标映射
|
|
||||||
const iconMap = {
|
|
||||||
GiftOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
SettingOutlined: SettingIcon,
|
|
||||||
LineChartOutlined,
|
|
||||||
DashboardOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 替换组件中的icon引用
|
|
||||||
Object.keys(allComps).forEach((key) => {
|
|
||||||
if (allComps[key].icon && typeof allComps[key].icon === 'string') {
|
|
||||||
allComps[key].icon = iconMap[allComps[key].icon] || GiftOutlined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const customizing = ref(false)
|
const customizing = ref(false)
|
||||||
const widgetsRef = ref(null)
|
const widgetsRef = ref(null)
|
||||||
const defaultGrid = {
|
const defaultGrid = config.DEFAULT_GRID
|
||||||
layout: [12, 6, 6],
|
|
||||||
compsList: [['welcome', 'info'], ['echarts', 'progress'], ['time', 'sms']],
|
|
||||||
}
|
|
||||||
const grid = reactive({ layout: [], compsList: [] })
|
const grid = reactive({ layout: [], compsList: [] })
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
@@ -252,7 +157,7 @@ const initGrid = () => {
|
|||||||
const parsed = JSON.parse(savedGrid)
|
const parsed = JSON.parse(savedGrid)
|
||||||
grid.layout = parsed.layout
|
grid.layout = parsed.layout
|
||||||
grid.compsList = parsed.compsList
|
grid.compsList = parsed.compsList
|
||||||
} catch (e) {
|
} catch {
|
||||||
resetToDefault()
|
resetToDefault()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -316,9 +221,9 @@ const addComp = (item) => {
|
|||||||
grid.compsList[0].push(item.key)
|
grid.compsList[0].push(item.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeComp = (key) => grid.compsList.forEach((list, index) => {
|
const removeComp = (key) => grid.compsList.forEach((list, index) => {
|
||||||
grid.compsList[index] = list.filter((k) => k !== key)
|
grid.compsList[index] = list.filter((k) => k !== key)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
customizing.value = false
|
customizing.value = false
|
||||||
@@ -588,9 +493,11 @@ const emit = defineEmits(['on-mounted'])
|
|||||||
.customizing .widgets {
|
.customizing .widgets {
|
||||||
transform: scale(1) !important;
|
transform: scale(1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customizing .widgets-drawer {
|
.customizing .widgets-drawer {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customizing .widgets-wrapper {
|
.customizing .widgets-wrapper {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,15 +21,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="apps-grid">
|
<div v-else class="apps-grid">
|
||||||
<draggable v-model="myApps" item-key="id" :animation="200" ghost-class="ghost" drag-class="dragging"
|
<draggable v-model="myApps" item-key="path" :animation="200" ghost-class="ghost" drag-class="dragging"
|
||||||
class="draggable-grid">
|
class="draggable-grid">
|
||||||
<template #item="{ element }">
|
<template #item="{ element }">
|
||||||
<div class="app-item" @click="handleAppClick(element)">
|
<div class="app-item" @click="handleAppClick(element)">
|
||||||
<div class="app-icon">
|
<div class="app-icon">
|
||||||
<component :is="element.icon" />
|
<component :is="getIconComponent(element.meta?.icon)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="app-name">{{ element.name }}</div>
|
<div class="app-name">{{ element.meta?.title }}</div>
|
||||||
<div class="app-description">{{ element.description }}</div>
|
<div class="app-description">{{ element.meta?.description || '点击打开' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<!-- 添加应用抽屉 -->
|
<!-- 添加应用抽屉 -->
|
||||||
<a-drawer v-model:open="drawerVisible" title="管理应用" :width="650" placement="right" class="app-drawer">
|
<a-drawer v-model:open="drawerVisible" title="管理应用" :width="650" placement="right">
|
||||||
<div class="drawer-content">
|
<div class="drawer-content">
|
||||||
<div class="app-section">
|
<div class="app-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -48,40 +48,25 @@
|
|||||||
<span class="count">{{ myApps.length }} 个应用</span>
|
<span class="count">{{ myApps.length }} 个应用</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="tips">拖拽卡片调整顺序,点击移除按钮移除应用</p>
|
<p class="tips">拖拽卡片调整顺序,点击移除按钮移除应用</p>
|
||||||
<div class="draggable-wrapper">
|
<draggable v-model="myApps" item-key="path" :animation="200" ghost-class="drawer-ghost"
|
||||||
<draggable
|
drag-class="drawer-dragging" class="drawer-grid" group="apps">
|
||||||
v-model="myApps"
|
<template #item="{ element }">
|
||||||
item-key="id"
|
<div class="drawer-app-card">
|
||||||
:animation="200"
|
<div class="remove-btn" @click.stop="removeApp(element.path)">
|
||||||
ghost-class="drawer-ghost"
|
<CloseOutlined />
|
||||||
drag-class="drawer-dragging"
|
|
||||||
class="draggable-list"
|
|
||||||
group="apps">
|
|
||||||
<template #item="{ element }">
|
|
||||||
<div class="drawer-app-item">
|
|
||||||
<div class="drag-handle">
|
|
||||||
<HolderOutlined />
|
|
||||||
</div>
|
|
||||||
<div class="app-icon">
|
|
||||||
<component :is="element.icon" />
|
|
||||||
</div>
|
|
||||||
<div class="app-info">
|
|
||||||
<div class="app-name">{{ element.name }}</div>
|
|
||||||
<div class="app-description">{{ element.description }}</div>
|
|
||||||
</div>
|
|
||||||
<a-button type="text" danger size="small" @click.stop="removeApp(element.id)">
|
|
||||||
<template #icon>
|
|
||||||
<CloseOutlined />
|
|
||||||
</template>
|
|
||||||
移除
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="app-icon">
|
||||||
</draggable>
|
<component :is="getIconComponent(element.meta?.icon)" />
|
||||||
<div v-if="myApps.length === 0" class="empty-zone">
|
</div>
|
||||||
<a-empty description="暂无常用应用" :image="false" />
|
<div class="app-name">{{ element.meta?.title }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div v-if="myApps.length === 0" class="empty-zone">
|
||||||
|
<a-empty description="暂无常用应用,从下方拖入应用" :image="false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-divider style="margin: 24px 0" />
|
<a-divider style="margin: 24px 0" />
|
||||||
@@ -95,34 +80,22 @@
|
|||||||
<span class="count">{{ allApps.length }} 个可用</span>
|
<span class="count">{{ allApps.length }} 个可用</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="tips">拖拽卡片到上方添加为常用应用</p>
|
<p class="tips">拖拽卡片到上方添加为常用应用</p>
|
||||||
<div class="draggable-wrapper">
|
<draggable v-model="allApps" item-key="path" :animation="200" ghost-class="drawer-ghost"
|
||||||
<draggable
|
drag-class="drawer-dragging" class="drawer-grid" group="apps">
|
||||||
v-model="allApps"
|
<template #item="{ element }">
|
||||||
item-key="id"
|
<div class="drawer-app-card">
|
||||||
:animation="200"
|
<div class="app-icon">
|
||||||
ghost-class="drawer-ghost"
|
<component :is="getIconComponent(element.meta?.icon)" />
|
||||||
drag-class="drawer-dragging"
|
|
||||||
class="draggable-list"
|
|
||||||
group="apps">
|
|
||||||
<template #item="{ element }">
|
|
||||||
<div class="drawer-app-item">
|
|
||||||
<div class="drag-handle">
|
|
||||||
<HolderOutlined />
|
|
||||||
</div>
|
|
||||||
<div class="app-icon">
|
|
||||||
<component :is="element.icon" />
|
|
||||||
</div>
|
|
||||||
<div class="app-info">
|
|
||||||
<div class="app-name">{{ element.name }}</div>
|
|
||||||
<div class="app-description">{{ element.description }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="app-name">{{ element.meta?.title }}</div>
|
||||||
</draggable>
|
</div>
|
||||||
<div v-if="allApps.length === 0" class="empty-zone">
|
</template>
|
||||||
<a-empty description="所有应用已添加" :image="false" />
|
<template #footer>
|
||||||
</div>
|
<div v-if="allApps.length === 0" class="empty-zone">
|
||||||
</div>
|
<a-empty description="所有应用已添加" :image="false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,47 +108,73 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
|
import * as icons from '@ant-design/icons-vue'
|
||||||
|
|
||||||
// 定义组件名称
|
// 定义组件名称
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MyApp',
|
name: 'MyApp',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 应用数据
|
const router = useRouter()
|
||||||
const allAppsList = [
|
const userStore = useUserStore()
|
||||||
{ id: 1, name: '用户管理', description: '管理系统用户', icon: 'UserOutlined', path: '/auth/user' },
|
|
||||||
{ id: 2, name: '角色管理', description: '管理系统角色', icon: 'SettingOutlined', path: '/auth/role' },
|
// 从菜单中提取所有应用项(扁平化菜单树)
|
||||||
{ id: 3, name: '日志管理', description: '查看系统日志', icon: 'FileTextOutlined', path: '/system/log' },
|
const extractMenuItems = (menus) => {
|
||||||
{ id: 4, name: '数据统计', description: '查看数据报表', icon: 'BarChartOutlined', path: '/statistics' },
|
const items = []
|
||||||
{ id: 5, name: '日程安排', description: '查看日程', icon: 'CalendarOutlined', path: '/schedule' },
|
|
||||||
{ id: 6, name: '消息中心', description: '查看消息', icon: 'MessageOutlined', path: '/messages' },
|
const traverse = (menuList) => {
|
||||||
{ id: 7, name: '订单管理', description: '管理订单', icon: 'ShoppingCartOutlined', path: '/orders' },
|
for (const menu of menuList) {
|
||||||
]
|
// 只添加有路径的菜单项(排除父级菜单项)
|
||||||
|
if (menu.path && (!menu.children || menu.children.length === 0)) {
|
||||||
|
items.push(menu)
|
||||||
|
}
|
||||||
|
// 递归处理子菜单
|
||||||
|
if (menu.children && menu.children.length > 0) {
|
||||||
|
traverse(menu.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(menus)
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有可用应用
|
||||||
|
const allAvailableApps = computed(() => {
|
||||||
|
return extractMenuItems(userStore.menu || [])
|
||||||
|
})
|
||||||
|
|
||||||
const drawerVisible = ref(false)
|
const drawerVisible = ref(false)
|
||||||
const myApps = ref([])
|
const myApps = ref([])
|
||||||
const allApps = ref([])
|
const allApps = ref([])
|
||||||
|
|
||||||
|
// 获取图标组件
|
||||||
|
const getIconComponent = (iconName) => {
|
||||||
|
return icons[iconName] || icons.FileTextOutlined
|
||||||
|
}
|
||||||
|
|
||||||
// 从本地存储加载数据
|
// 从本地存储加载数据
|
||||||
const loadApps = () => {
|
const loadApps = () => {
|
||||||
const savedApps = localStorage.getItem('myApps')
|
const savedApps = localStorage.getItem('myApps')
|
||||||
if (savedApps) {
|
if (savedApps) {
|
||||||
const savedIds = JSON.parse(savedApps)
|
const savedPaths = JSON.parse(savedApps)
|
||||||
myApps.value = allAppsList.filter(app => savedIds.includes(app.id))
|
myApps.value = allAvailableApps.value.filter(app => savedPaths.includes(app.path))
|
||||||
} else {
|
} else {
|
||||||
// 默认显示前4个应用
|
// 默认显示前4个应用
|
||||||
myApps.value = allAppsList.slice(0, 4)
|
myApps.value = allAvailableApps.value.slice(0, 4)
|
||||||
}
|
}
|
||||||
updateAllApps()
|
updateAllApps()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新全部应用列表(排除已添加的)
|
// 更新全部应用列表(排除已添加的)
|
||||||
const updateAllApps = () => {
|
const updateAllApps = () => {
|
||||||
const myAppIds = myApps.value.map(app => app.id)
|
const myAppPaths = myApps.value.map(app => app.path)
|
||||||
allApps.value = allAppsList.filter(app => !myAppIds.includes(app.id))
|
allApps.value = allAvailableApps.value.filter(app => !myAppPaths.includes(app.path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示抽屉
|
// 显示抽屉
|
||||||
@@ -185,8 +184,8 @@ const showDrawer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 移除应用
|
// 移除应用
|
||||||
const removeApp = (id) => {
|
const removeApp = (path) => {
|
||||||
const index = myApps.value.findIndex(app => app.id === id)
|
const index = myApps.value.findIndex(app => app.path === path)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
myApps.value.splice(index, 1)
|
myApps.value.splice(index, 1)
|
||||||
updateAllApps()
|
updateAllApps()
|
||||||
@@ -195,20 +194,32 @@ const removeApp = (id) => {
|
|||||||
|
|
||||||
// 应用点击处理
|
// 应用点击处理
|
||||||
const handleAppClick = (app) => {
|
const handleAppClick = (app) => {
|
||||||
// 这里可以跳转到对应的路由
|
if (app.path) {
|
||||||
message.info(`打开应用: ${app.name}`)
|
router.push(app.path)
|
||||||
|
} else {
|
||||||
|
message.warning('该应用没有配置路由')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存设置
|
// 保存设置
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const appIds = myApps.value.map(app => app.id)
|
const appPaths = myApps.value.map(app => app.path)
|
||||||
localStorage.setItem('myApps', JSON.stringify(appIds))
|
localStorage.setItem('myApps', JSON.stringify(appPaths))
|
||||||
message.success('保存成功')
|
message.success('保存成功')
|
||||||
drawerVisible.value = false
|
drawerVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
loadApps()
|
loadApps()
|
||||||
|
|
||||||
|
// 监听菜单变化,重新加载应用
|
||||||
|
watch(
|
||||||
|
() => userStore.menu,
|
||||||
|
() => {
|
||||||
|
loadApps()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -269,6 +280,9 @@ loadApps()
|
|||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-name {
|
.app-name {
|
||||||
@@ -285,166 +299,171 @@ loadApps()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 抽屉样式(不使用 scoped 的 deep,直接使用全局样式)
|
// 抽屉样式 - 使用全局样式避免深度问题
|
||||||
.my-app .app-drawer {
|
:deep(.my-app .ant-drawer-body) {
|
||||||
.ant-drawer-body {
|
padding: 16px;
|
||||||
padding: 16px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.ant-drawer-footer {
|
:deep(.my-app .ant-drawer-footer) {
|
||||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-content {
|
.drawer-content {
|
||||||
.app-section {
|
.app-section {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 6px;
|
||||||
margin-bottom: 8px;
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: #262626;
|
||||||
|
|
||||||
h3 {
|
.anticon {
|
||||||
display: flex;
|
font-size: 18px;
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
color: #262626;
|
|
||||||
|
|
||||||
.anticon {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.count {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tips {
|
.count {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #8c8c8c;
|
color: #999;
|
||||||
margin-bottom: 12px;
|
background: #f5f5f5;
|
||||||
line-height: 1.5;
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-zone {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.ant-empty-description {
|
||||||
|
color: #bfbfbf;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽屉内卡片式网格布局
|
||||||
|
.drawer-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-app-card {
|
||||||
|
position: relative;
|
||||||
|
padding: 20px 16px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: grab;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable-wrapper {
|
&:hover {
|
||||||
position: relative;
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
|
||||||
.draggable-list {
|
transform: translateY(-2px);
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-zone {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px 20px;
|
|
||||||
background: #fafafa;
|
|
||||||
border: 2px dashed #d9d9d9;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
.ant-empty-description {
|
|
||||||
color: #bfbfbf;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-app-item {
|
.remove-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
justify-content: center;
|
||||||
background: #fff;
|
background: #ff4d4f;
|
||||||
border: 1px solid #f0f0f0;
|
color: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 50%;
|
||||||
margin-bottom: 12px;
|
font-size: 12px;
|
||||||
transition: all 0.25s ease;
|
cursor: pointer;
|
||||||
cursor: grab;
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s;
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #1890ff;
|
background: #ff7875;
|
||||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
|
|
||||||
transform: translateX(2px);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.drag-handle {
|
&:hover .remove-btn {
|
||||||
font-size: 16px;
|
opacity: 1;
|
||||||
color: #bfbfbf;
|
}
|
||||||
margin-right: 12px;
|
|
||||||
cursor: grab;
|
|
||||||
transition: color 0.25s;
|
|
||||||
|
|
||||||
&:hover {
|
.app-icon {
|
||||||
color: #1890ff;
|
width: 48px;
|
||||||
}
|
height: 48px;
|
||||||
}
|
margin: 0 auto 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #1890ff;
|
||||||
|
|
||||||
.app-icon {
|
:deep(.anticon) {
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #1890ff;
|
|
||||||
margin-right: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.app-name {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #262626;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-description {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽时的样式
|
.app-name {
|
||||||
.drawer-ghost {
|
font-size: 14px;
|
||||||
opacity: 0.4;
|
font-weight: 500;
|
||||||
background: #e6f7ff;
|
color: #262626;
|
||||||
border-color: #1890ff;
|
margin-bottom: 4px;
|
||||||
border-style: dashed;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-dragging {
|
.app-description {
|
||||||
opacity: 0.6;
|
font-size: 12px;
|
||||||
transform: scale(0.98);
|
color: #8c8c8c;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 拖拽时的样式
|
||||||
|
.drawer-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
background: #e6f7ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-dragging {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user