Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

添加 useDialog hooks #496

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,6 @@
"yiwen",
"zhongyingwen",
"zhuti"
]
],
"i18n-ally.localesPaths": ["src/languages"]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"echarts": "^5.5.1",
"echarts-liquidfill": "^3.1.0",
"element-plus": "^2.7.6",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
Expand Down
2 changes: 1 addition & 1 deletion src/api/modules/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const loginApi = (params: Login.ReqLoginForm) => {

// 获取菜单列表
export const getAuthMenuListApi = () => {
return http.get<Menu.MenuOptions[]>(PORT1 + `/menu/list`, {}, { loading: false });
// return http.get<Menu.MenuOptions[]>(PORT1 + `/menu/list`, {}, { loading: false });
// 如果想让菜单变为本地数据,注释上一行代码,并引入本地 authMenuList.json 数据
return authMenuList;
};
Expand Down
14 changes: 14 additions & 0 deletions src/assets/json/authMenuList.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,20 @@
"isKeepAlive": true
}
},
{
"path": "/assembly/dialog/useDialog",
"name": "useDialog",
"component": "/assembly/dialog/useDialog/index",
"meta": {
"icon": "Menu",
"title": "useDialog弹窗操作",
"isLink": "",
"isHide": false,
"isFull": false,
"isAffix": false,
"isKeepAlive": true
}
},
{
"path": "/assembly/tabs",
"name": "tabs",
Expand Down
135 changes: 135 additions & 0 deletions src/hooks/useDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { ElDialog } from "element-plus";
import type { ComponentInternalInstance, Ref } from "vue";
import { getCurrentInstance, h, isRef, onUnmounted, render } from "vue";
import { upperFirst } from "lodash";
import merge from "lodash/merge";
import type { JSX } from "vue/jsx-runtime";

type Content = Parameters<typeof h>[0] | string | Object | JSX.Element;
// 使用 InstanceType 获取 ElDialog 组件实例的类型
type ElDialogInstance = InstanceType<typeof ElDialog>;

// 从组件实例中提取 Props 类型
type DialogProps = ElDialogInstance["$props"] & {
onBeforeOpen?: () => boolean | void; // 支持显示之前钩子
};

interface ElDialogSlots {
header?: (...args: any[]) => Content;
footer?: (...args: any[]) => Content;
}

interface Options<P> {
dialogProps?: DialogProps;
dialogSlots?: ElDialogSlots;
contentProps?: P;
callBack?: Function;
closeEventName?: string;
}

// 定义工具函数,获取计算属性的option
function getOptions<P>(options?: Ref<Options<P>> | Options<P>) {
if (!options) return {};
return isRef(options) ? options.value : options;
}

export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
let dialogInstance: ComponentInternalInstance | null = null;
let fragment: Element | null = null;

// 关闭并卸载组件
const closeAfter = () => {
if (fragment) {
render(null, fragment as unknown as Element); // 卸载组件
fragment.textContent = ""; // 清空文档片段
fragment = null;
}
dialogInstance = null;
};

// 关闭对话框
async function closeDialog() {
if (dialogInstance) dialogInstance.props.modelValue = false;
}

// 获取当前组件实例,用于设置当前dialog的上下文,继承prototype
const instance = getCurrentInstance();

// 创建并挂载组件
function openDialog(modifyOptions?: Partial<Options<P>>) {
if (dialogInstance) {
closeDialog();
closeAfter();
}
const _options = getOptions(options);
// 如果有修改,则合并options。替换之前的options变量为 _options
if (modifyOptions) merge(_options, modifyOptions);
const { dialogProps, contentProps } = _options;

// 调用before钩子,如果为false则不打开
if (dialogProps?.onBeforeOpen?.() === false) {
return;
}
fragment = document.createDocumentFragment() as unknown as Element;

// 转换closeEventName事件
const closeEventName = `on${upperFirst(_options?.closeEventName || "closeDialog")}`;

// 定义当前块关闭前钩子变量
let onBeforeClose: (() => Promise<boolean | void> | boolean | void) | null;

const vNode = h(
ElDialog,
{
...dialogProps,
modelValue: true,
beforeClose: async done => {
// 配置`el-dialog`的关闭回调钩子函数
const result = await onBeforeClose?.();
if (result === false) {
return;
}
done();
},
onClosed: () => {
dialogProps?.onClosed?.();
closeAfter();
// 关闭后回收当前变量
onBeforeClose = null;
}
},
{
default: () => [
typeof content === "string"
? content
: h(content as any, {
...contentProps,
[closeEventName]: closeDialog, // 监听自定义关闭事件,并执行关闭
// 监听自定义回调事件,并执行关闭
callBack: (data: any) => {
if (_options.callBack) _options.callBack(data);
closeDialog();
},
beforeCloseDialog: (fn: () => boolean | void) => {
// 把`beforeCloseDialog`传递给`content`,当组件内部使用`props.beforeCloseDialog(fn)`时,会把fn传递给`onBeforeClose`
onBeforeClose = fn;
}
})
],
..._options.dialogSlots
}
);

// 设置当前的上下文为使用者的上下文
vNode.appContext = instance?.appContext || null;
render(vNode, fragment);
dialogInstance = vNode.component;
document.body.appendChild(fragment);
}

onUnmounted(() => {
closeDialog();
});

return { openDialog, closeDialog };
}
93 changes: 93 additions & 0 deletions src/hooks/useECharts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as echarts from "echarts";
import { onActivated, onDeactivated, onMounted, onUnmounted, reactive, Ref, shallowRef, watch } from "vue";

// 定义一个策略接口 , 方便具体策略实现使用
export interface ChartStrategy {
getOptions: () => echarts.EChartsOption;
}

// 自定义的 Vue 钩子函数,用于管理 ECharts 实例
export function useECharts(
chartRef: Ref<HTMLElement | null>, // chartRef 是一个引用,指向包含图表的 DOM 元素
initOptions: echarts.EChartsOption, // strategy 是一个接口,提供获取图表配置的方法
theme?: string | object | null, // 可选的 theme 参数,用于设置 ECharts 主题
opts?: echarts.EChartsInitOpts // 可选的 opts 参数,用于初始化 ECharts 的配置
) {
// 使用 shallowRef 创建一个响应式引用,用于保存 ECharts 实例
const instance = shallowRef<echarts.EChartsType | null>(null);

// 使用 ref 创建一个响应式引用,用于保存图表的配置选项
const options = reactive<echarts.EChartsOption>(initOptions);

// 初始化图表实例的函数
const initEchart = () => {
// 确保 chartRef 绑定了 DOM 元素且 instance 尚未初始化
if (chartRef.value && !instance.value) {
// 初始化 ECharts 实例并赋值给 instance
instance.value = echarts.init(chartRef.value, theme, opts);
// 设置图表的初始选项
instance.value.setOption(options);
}
};

// 更新图表配置选项的函数
const updateEchartOptions = (newOptions: echarts.EChartsOption) => {
if (instance.value) {
// 使用新的选项更新图表,不合并现有选项并延迟更新以优化性能
instance.value.setOption(newOptions, { notMerge: true, lazyUpdate: true });
}
};

// 处理窗口大小调整的函数,确保图表能够自动调整大小
const handleResize = () => {
instance.value?.resize(); // 如果 instance 存在,则调用 resize 方法
};

// 销毁图表实例的函数,释放内存并清空引用
const disposeEchart = () => {
instance.value?.dispose(); // 调用 ECharts 的 dispose 方法销毁实例
instance.value = null; // 清空 instance 引用,避免内存泄漏
};

// 监听 options 的变化,并在其发生改变时更新图表
watch(
() => options,
() => updateEchartOptions,
{ deep: true }
);

// 组件挂载时初始化图表并添加窗口大小调整的事件监听器
onMounted(() => {
initEchart(); // 初始化图表
window.addEventListener("resize", handleResize); // 监听窗口大小变化
});

// 组件卸载时移除事件监听器并销毁图表实例
onUnmounted(() => {
window.removeEventListener("resize", handleResize); // 移除窗口大小调整的监听器
disposeEchart(); // 销毁图表实例
});

// 组件激活时重新初始化图表并添加事件监听器
onActivated(() => {
if (!instance.value) {
initEchart(); // 如果图表实例不存在,重新初始化
}
window.addEventListener("resize", handleResize); // 监听窗口大小变化
});

// 组件停用时移除事件监听器并销毁图表实例
onDeactivated(() => {
window.removeEventListener("resize", handleResize); // 移除窗口大小调整的监听器
disposeEchart(); // 销毁图表实例
});

// 返回 instance 和相关的控制方法,供外部组件使用
return {
instance, // 返回图表实例的引用
options, // 返回图表配置选项的引用
initEchart, // 返回初始化图表的方法
handleResize, // 返回手动触发图表调整大小的方法
disposeEchart // 返回手动销毁图表实例的方法
};
}
58 changes: 58 additions & 0 deletions src/views/assembly/dialog/useDialog/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<div class="card content-box">
<span class="text"> useDialog hooks 操作弹框🍓🍇🍈🍉</span>

<el-button type="primary" @click="openDialog">打开弹框</el-button>

<div>
<span class="title">useDialog hooks介绍 📚</span>
<p>
在 Vue 中,Hook 允许在函数式组件或者 API 中「钩入」Vue 特性。它们通常在组合式 API(Composition API)中使用,这是 Vue
提供的一套响应式和可复用逻辑功能的集合。 本文提到的 useDialog Hook 就是一个封装了 el-dialog 组件基本功能的自定义
Hook,它还可以提供附加特性以便在项目中管理和展示弹窗。
</p>
<span class="title">主要实现功能📚</span>
<ul>
<li>满足基础用法,传入 el-dialog 的基础属性以及默认slot显示的内容,导出 openDialog 和 closeDialog 函数;</li>
<li>支持 el-dialog 的事件配置;</li>
<li>支持默认 slot 组件的属性配置;</li>
<li>支持 el-dialog 其他 slot 配置,例如 header 和 footer 等;</li>
<li>在内容组件中抛出特定事件支持关闭 dialog;</li>
<li>支持显示内容为 jsx、普通文本、Vue Component;</li>
<li>支持在显示内容中控制是否可以关闭的回调函数,例如 beforeClose;</li>
<li>支持显示之前钩子,例如 onBeforeOpen;</li>
<li>支持定义和弹出时修改配置属性;</li>
<li>支持继承 root vue 的 prototype,可以使用如 vue-i18n 的 $t 函数;</li>
<li>支持 ts 参数提示;</li>
</ul>
</div>
</div>
</template>

<script lang="ts" name="useDialog" setup>
import { defineAsyncComponent } from "vue";
import { useDialog } from "@/hooks/useDialog";

const test = defineAsyncComponent(() => import("./test.vue"));

const { openDialog } = useDialog(test, {
dialogProps: {
title: "测试弹窗",
width: "800px"
},
contentProps: {
from: "测试弹窗"
},
callBack: (data: any) => {
// ElMessage.success("回调事件", data.from.value);
console.log(data, "data");
}
});
</script>

<style lang="scss" scoped>
.title {
font-size: 18px;
font-weight: bold;
}
</style>
Loading