release: update 5.1.0
@@ -27,8 +27,11 @@ const addDialog = (options: DialogOptions) => {
|
||||
|
||||
/** 关闭弹框 */
|
||||
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
|
||||
dialogStore.value.splice(index, 1);
|
||||
dialogStore.value[index].visible = false;
|
||||
options.closeCallBack && options.closeCallBack({ options, index, args });
|
||||
useTimeoutFn(() => {
|
||||
dialogStore.value.splice(index, 1);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -49,7 +52,7 @@ const closeAllDialog = () => {
|
||||
/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L13
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L18
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L20
|
||||
*/
|
||||
const ReDialog = withInstall(reDialog);
|
||||
|
||||
|
@@ -84,13 +84,13 @@ function handleClose(
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
class="pure-dialog"
|
||||
v-for="(options, index) in dialogStore"
|
||||
:key="index"
|
||||
v-bind="options"
|
||||
v-model="options.visible"
|
||||
class="pure-dialog"
|
||||
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
|
||||
@close="handleClose(options, index)"
|
||||
@closed="handleClose(options, index)"
|
||||
@opened="eventsCallBack('open', options, index)"
|
||||
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
|
||||
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
|
||||
@@ -116,15 +116,15 @@ function handleClose(
|
||||
options?.fullscreen
|
||||
? ExitFullscreen
|
||||
: fullscreen
|
||||
? ExitFullscreen
|
||||
: Fullscreen
|
||||
? ExitFullscreen
|
||||
: Fullscreen
|
||||
"
|
||||
/>
|
||||
</i>
|
||||
</div>
|
||||
<component
|
||||
v-else
|
||||
:is="options?.headerRenderer({ close, titleId, titleClass })"
|
||||
v-else
|
||||
/>
|
||||
</template>
|
||||
<component
|
||||
|
@@ -3,7 +3,7 @@ import type { CSSProperties, VNode, Component } from "vue";
|
||||
type DoneFn = (cancel?: boolean) => void;
|
||||
type EventType = "open" | "close" | "openAutoFocus" | "closeAutoFocus";
|
||||
type ArgsType = {
|
||||
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或者空白页 */
|
||||
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
|
||||
command: "cancel" | "sure" | "close";
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ interface DialogOptions extends DialogProps {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}) => void;
|
||||
/** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或者空白页关闭页面时才会触发) */
|
||||
/** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发) */
|
||||
close?: ({
|
||||
options,
|
||||
index
|
||||
@@ -165,7 +165,7 @@ interface DialogOptions extends DialogProps {
|
||||
options: DialogOptions;
|
||||
index: number;
|
||||
}) => void;
|
||||
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或者空白页 */
|
||||
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
|
||||
closeCallBack?: ({
|
||||
options,
|
||||
index,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { iconType } from "./types";
|
||||
import { h, defineComponent, Component } from "vue";
|
||||
import type { iconType } from "./types";
|
||||
import { h, defineComponent, type Component } from "vue";
|
||||
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ export function useRenderIcon(icon: any, attrs?: iconType): Component {
|
||||
});
|
||||
} else if (typeof icon === "function" || typeof icon?.render === "function") {
|
||||
// svg
|
||||
return icon;
|
||||
return attrs ? h(icon, { ...attrs }) : icon;
|
||||
} else if (typeof icon === "object") {
|
||||
return defineComponent({
|
||||
name: "OfflineIcon",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { h, defineComponent } from "vue";
|
||||
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
|
||||
|
||||
// Iconify Icon在Vue里本地使用(用于内网环境)https://docs.iconify.design/icon-components/vue/offline.html
|
||||
// Iconify Icon在Vue里本地使用(用于内网环境)
|
||||
export default defineComponent({
|
||||
name: "IconifyIconOffline",
|
||||
components: { IconifyIcon },
|
||||
|
@@ -1,14 +1,14 @@
|
||||
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
|
||||
import { addIcon } from "@iconify/vue/dist/offline";
|
||||
|
||||
/**
|
||||
* 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
|
||||
*/
|
||||
|
||||
// 本地菜单图标,后端在路由的icon中返回对应的图标字符串并且前端在此处使用addIcon添加即可渲染菜单图标
|
||||
import HomeFilled from "@iconify-icons/ep/home-filled";
|
||||
import InformationLine from "@iconify-icons/ri/information-line";
|
||||
// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标
|
||||
// @iconify-icons/ep
|
||||
import Lollipop from "@iconify-icons/ep/lollipop";
|
||||
|
||||
addIcon("homeFilled", HomeFilled);
|
||||
addIcon("informationLine", InformationLine);
|
||||
addIcon("lollipop", Lollipop);
|
||||
import HomeFilled from "@iconify-icons/ep/home-filled";
|
||||
addIcon("ep:lollipop", Lollipop);
|
||||
addIcon("ep:home-filled", HomeFilled);
|
||||
// @iconify-icons/ri
|
||||
import Search from "@iconify-icons/ri/search-line";
|
||||
import InformationLine from "@iconify-icons/ri/information-line";
|
||||
addIcon("ri:search-line", Search);
|
||||
addIcon("ri:information-line", InformationLine);
|
||||
|
@@ -13,7 +13,8 @@ export interface iconType {
|
||||
align?: string;
|
||||
onLoad?: Function;
|
||||
includes?: Function;
|
||||
|
||||
// all icon
|
||||
// svg 需要什么SVG属性自行添加
|
||||
fill?: string;
|
||||
// all icon
|
||||
style?: object;
|
||||
}
|
||||
|
@@ -29,6 +29,10 @@ const props = {
|
||||
columns: {
|
||||
type: Array as PropType<TableColumnList>,
|
||||
default: () => []
|
||||
},
|
||||
isExpandAll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,12 +41,11 @@ export default defineComponent({
|
||||
props,
|
||||
emits: ["refresh"],
|
||||
setup(props, { emit, slots, attrs }) {
|
||||
const buttonRef = ref();
|
||||
const size = ref("default");
|
||||
const isExpandAll = ref(true);
|
||||
const loading = ref(false);
|
||||
const checkAll = ref(true);
|
||||
const isIndeterminate = ref(false);
|
||||
const isExpandAll = ref(props.isExpandAll);
|
||||
const filterColumns = cloneDeep(props?.columns).filter(column =>
|
||||
isBoolean(column?.hide)
|
||||
? !column.hide
|
||||
@@ -200,11 +203,22 @@ export default defineComponent({
|
||||
: false;
|
||||
};
|
||||
|
||||
const rendTippyProps = (content: string) => {
|
||||
// https://vue-tippy.netlify.app/props
|
||||
return {
|
||||
content,
|
||||
offset: [0, 18],
|
||||
duration: [300, 0],
|
||||
followCursor: true,
|
||||
hideOnClick: "toggle"
|
||||
};
|
||||
};
|
||||
|
||||
const reference = {
|
||||
reference: () => (
|
||||
<SettingIcon
|
||||
class={["w-[16px]", iconClass.value]}
|
||||
onMouseover={e => (buttonRef.value = e.currentTarget)}
|
||||
v-tippy={rendTippyProps("列设置")}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@@ -224,45 +238,43 @@ export default defineComponent({
|
||||
) : null}
|
||||
{props.tableRef?.size ? (
|
||||
<>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content={isExpandAll.value ? "折叠" : "展开"}
|
||||
placement="top"
|
||||
>
|
||||
<ExpandIcon
|
||||
class={["w-[16px]", iconClass.value]}
|
||||
style={{
|
||||
transform: isExpandAll.value ? "none" : "rotate(-90deg)"
|
||||
}}
|
||||
onClick={() => onExpand()}
|
||||
/>
|
||||
</el-tooltip>
|
||||
<ExpandIcon
|
||||
class={["w-[16px]", iconClass.value]}
|
||||
style={{
|
||||
transform: isExpandAll.value ? "none" : "rotate(-90deg)"
|
||||
}}
|
||||
v-tippy={rendTippyProps(
|
||||
isExpandAll.value ? "折叠" : "展开"
|
||||
)}
|
||||
onClick={() => onExpand()}
|
||||
/>
|
||||
<el-divider direction="vertical" />
|
||||
</>
|
||||
) : null}
|
||||
<el-tooltip effect="dark" content="刷新" placement="top">
|
||||
<RefreshIcon
|
||||
class={[
|
||||
"w-[16px]",
|
||||
iconClass.value,
|
||||
loading.value ? "animate-spin" : ""
|
||||
]}
|
||||
onClick={() => onReFresh()}
|
||||
/>
|
||||
</el-tooltip>
|
||||
<RefreshIcon
|
||||
class={[
|
||||
"w-[16px]",
|
||||
iconClass.value,
|
||||
loading.value ? "animate-spin" : ""
|
||||
]}
|
||||
v-tippy={rendTippyProps("刷新")}
|
||||
onClick={() => onReFresh()}
|
||||
/>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tooltip effect="dark" content="密度" placement="top">
|
||||
<el-dropdown v-slots={dropdown} trigger="click">
|
||||
<CollapseIcon class={["w-[16px]", iconClass.value]} />
|
||||
</el-dropdown>
|
||||
</el-tooltip>
|
||||
<el-dropdown
|
||||
v-slots={dropdown}
|
||||
trigger="click"
|
||||
v-tippy={rendTippyProps("密度")}
|
||||
>
|
||||
<CollapseIcon class={["w-[16px]", iconClass.value]} />
|
||||
</el-dropdown>
|
||||
<el-divider direction="vertical" />
|
||||
|
||||
<el-popover
|
||||
v-slots={reference}
|
||||
placement="bottom-start"
|
||||
popper-style={{ padding: 0 }}
|
||||
width="160"
|
||||
width="200"
|
||||
trigger="click"
|
||||
>
|
||||
<div class={[topClass.value]}>
|
||||
@@ -279,70 +291,53 @@ export default defineComponent({
|
||||
</div>
|
||||
|
||||
<div class="pt-[6px] pl-[11px]">
|
||||
<el-checkbox-group
|
||||
v-model={checkedColumns.value}
|
||||
onChange={value => handleCheckedColumnsChange(value)}
|
||||
>
|
||||
<el-space
|
||||
direction="vertical"
|
||||
alignment="flex-start"
|
||||
size={0}
|
||||
<el-scrollbar max-height="36vh">
|
||||
<el-checkbox-group
|
||||
v-model={checkedColumns.value}
|
||||
onChange={value => handleCheckedColumnsChange(value)}
|
||||
>
|
||||
{checkColumnList.map(item => {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
<DragIcon
|
||||
class={[
|
||||
"drag-btn w-[16px] mr-2",
|
||||
isFixedColumn(item)
|
||||
? "!cursor-no-drop"
|
||||
: "!cursor-grab"
|
||||
]}
|
||||
onMouseenter={(event: {
|
||||
preventDefault: () => void;
|
||||
}) => rowDrop(event)}
|
||||
/>
|
||||
<el-checkbox
|
||||
key={item}
|
||||
label={item}
|
||||
onChange={value =>
|
||||
handleCheckColumnListChange(value, item)
|
||||
}
|
||||
>
|
||||
<span
|
||||
title={item}
|
||||
class="inline-block w-[120px] truncate hover:text-text_color_primary"
|
||||
<el-space
|
||||
direction="vertical"
|
||||
alignment="flex-start"
|
||||
size={0}
|
||||
>
|
||||
{checkColumnList.map(item => {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
<DragIcon
|
||||
class={[
|
||||
"drag-btn w-[16px] mr-2",
|
||||
isFixedColumn(item)
|
||||
? "!cursor-no-drop"
|
||||
: "!cursor-grab"
|
||||
]}
|
||||
onMouseenter={(event: {
|
||||
preventDefault: () => void;
|
||||
}) => rowDrop(event)}
|
||||
/>
|
||||
<el-checkbox
|
||||
key={item}
|
||||
value={item}
|
||||
onChange={value =>
|
||||
handleCheckColumnListChange(value, item)
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</el-space>
|
||||
</el-checkbox-group>
|
||||
<span
|
||||
title={item}
|
||||
class="inline-block w-[120px] truncate hover:text-text_color_primary"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</el-space>
|
||||
</el-checkbox-group>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<el-tooltip
|
||||
popper-options={{
|
||||
modifiers: [
|
||||
{
|
||||
name: "computeStyles",
|
||||
options: {
|
||||
adaptive: false,
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
placement="top"
|
||||
virtual-ref={buttonRef.value}
|
||||
virtual-triggering
|
||||
trigger="hover"
|
||||
content="列设置"
|
||||
/>
|
||||
</div>
|
||||
{slots.default({
|
||||
size: size.value,
|
||||
|
@@ -1 +1 @@
|
||||
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13.79 10.21a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42l-2.5-2.5a1 1 0 0 0-.33-.21 1 1 0 0 0-.76 0 1 1 0 0 0-.33.21l-2.5 2.5a1 1 0 0 0 1.42 1.42l.79-.8v5.18l-.79-.8a1 1 0 0 0-1.42 1.42l2.5 2.5a1 1 0 0 0 .33.21.94.94 0 0 0 .76 0 1 1 0 0 0 .33-.21l2.5-2.5a1 1 0 0 0-1.42-1.42l-.79.8V9.41ZM7 4h10a1 1 0 0 0 0-2H7a1 1 0 0 0 0 2Zm10 16H7a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2Z"/></svg>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13.79 10.21a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42l-2.5-2.5a1 1 0 0 0-.33-.21 1 1 0 0 0-.76 0 1 1 0 0 0-.33.21l-2.5 2.5a1 1 0 0 0 1.42 1.42l.79-.8v5.18l-.79-.8a1 1 0 0 0-1.42 1.42l2.5 2.5a1 1 0 0 0 .33.21.94.94 0 0 0 .76 0 1 1 0 0 0 .33-.21l2.5-2.5a1 1 0 0 0-1.42-1.42l-.79.8V9.41ZM7 4h10a1 1 0 0 0 0-2H7a1 1 0 0 0 0 2m10 16H7a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2"/></svg>
|
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 439 B |
@@ -1 +1 @@
|
||||
<svg width="32" height="32" fill="currentColor" aria-hidden="true" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97zm0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97zM640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0zm0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0zM300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97zM640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0z"/></svg>
|
||||
<svg width="32" height="32" fill="currentColor" aria-hidden="true" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/></svg>
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 392 B |
@@ -1 +1 @@
|
||||
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4h9Z"/></svg>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4z"/></svg>
|
Before Width: | Height: | Size: 163 B After Width: | Height: | Size: 161 B |
@@ -1 +1 @@
|
||||
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M3.34 17a10.018 10.018 0 0 1-.978-2.326 3 3 0 0 0 .002-5.347A9.99 9.99 0 0 1 4.865 4.99a3 3 0 0 0 4.631-2.674 9.99 9.99 0 0 1 5.007.002 3 3 0 0 0 4.632 2.672A9.99 9.99 0 0 1 20.66 7c.433.749.757 1.53.978 2.326a3 3 0 0 0-.002 5.347 9.99 9.99 0 0 1-2.501 4.337 3 3 0 0 0-4.631 2.674 9.99 9.99 0 0 1-5.007-.002 3 3 0 0 0-4.632-2.672A10.018 10.018 0 0 1 3.34 17zm5.66.196a4.993 4.993 0 0 1 2.25 2.77c.499.047 1 .048 1.499.001A4.993 4.993 0 0 1 15 17.197a4.993 4.993 0 0 1 3.525-.565c.29-.408.54-.843.748-1.298A4.993 4.993 0 0 1 18 12c0-1.26.47-2.437 1.273-3.334a8.126 8.126 0 0 0-.75-1.298A4.993 4.993 0 0 1 15 6.804a4.993 4.993 0 0 1-2.25-2.77c-.499-.047-1-.048-1.499-.001A4.993 4.993 0 0 1 9 6.803a4.993 4.993 0 0 1-3.525.565 7.99 7.99 0 0 0-.748 1.298A4.993 4.993 0 0 1 6 12a4.99 4.99 0 0 1-1.273 3.334 8.126 8.126 0 0 0 .75 1.298A4.993 4.993 0 0 1 9 17.196zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M3.34 17a10 10 0 0 1-.978-2.326 3 3 0 0 0 .002-5.347A10 10 0 0 1 4.865 4.99a3 3 0 0 0 4.631-2.674 10 10 0 0 1 5.007.002 3 3 0 0 0 4.632 2.672A10 10 0 0 1 20.66 7c.433.749.757 1.53.978 2.326a3 3 0 0 0-.002 5.347 10 10 0 0 1-2.501 4.337 3 3 0 0 0-4.631 2.674 10 10 0 0 1-5.007-.002 3 3 0 0 0-4.632-2.672A10 10 0 0 1 3.34 17m5.66.196a5 5 0 0 1 2.25 2.77q.75.071 1.499.001A5 5 0 0 1 15 17.197a5 5 0 0 1 3.525-.565q.435-.614.748-1.298A5 5 0 0 1 18 12c0-1.26.47-2.437 1.273-3.334a8 8 0 0 0-.75-1.298A5 5 0 0 1 15 6.804a5 5 0 0 1-2.25-2.77q-.75-.071-1.499-.001A5 5 0 0 1 9 6.803a5 5 0 0 1-3.525.565 8 8 0 0 0-.748 1.298A5 5 0 0 1 6 12a5 5 0 0 1-1.273 3.334 8 8 0 0 0 .75 1.298A5 5 0 0 1 9 17.196M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>
|
Before Width: | Height: | Size: 1011 B After Width: | Height: | Size: 840 B |
8
src/components/ReSegmented/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import reSegmented from "./src/index";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
|
||||
/** 分段控制器组件 */
|
||||
export const ReSegmented = withInstall(reSegmented);
|
||||
|
||||
export default ReSegmented;
|
||||
export type { OptionsType } from "./src/type";
|
80
src/components/ReSegmented/src/index.css
Normal file
@@ -0,0 +1,80 @@
|
||||
.pure-segmented {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
background-color: rgb(0 0 0 / 4%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pure-segmented-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pure-segmented-item-selected {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
padding: 4px 0;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow:
|
||||
0 2px 8px -2px rgb(0 0 0 / 5%),
|
||||
0 1px 4px -1px rgb(0 0 0 / 7%),
|
||||
0 0 1px rgb(0 0 0 / 7%);
|
||||
transition:
|
||||
transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||
width 0.5s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
will-change: transform, width;
|
||||
}
|
||||
|
||||
.pure-segmented-item {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
.pure-segmented-item > div {
|
||||
min-height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 11px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pure-segmented-item > input {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pure-segmented-item-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pure-segmented-item-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.pure-segmented-item-disabled {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: not-allowed;
|
||||
}
|
166
src/components/ReSegmented/src/index.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import "./index.css";
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
toRef,
|
||||
watch,
|
||||
nextTick,
|
||||
defineComponent,
|
||||
getCurrentInstance
|
||||
} from "vue";
|
||||
import type { OptionsType } from "./type";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import { isFunction, isNumber, useDark } from "@pureadmin/utils";
|
||||
|
||||
const props = {
|
||||
options: {
|
||||
type: Array<OptionsType>,
|
||||
default: () => []
|
||||
},
|
||||
/** 默认选中,按照第一个索引为 `0` 的模式,可选(`modelValue`只有传`number`类型时才为响应式) */
|
||||
modelValue: {
|
||||
type: undefined,
|
||||
require: false,
|
||||
default: "0"
|
||||
}
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "ReSegmented",
|
||||
props,
|
||||
emits: ["change", "update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const width = ref(0);
|
||||
const translateX = ref(0);
|
||||
const { isDark } = useDark();
|
||||
const initStatus = ref(false);
|
||||
const curMouseActive = ref(-1);
|
||||
const segmentedItembg = ref("");
|
||||
const instance = getCurrentInstance()!;
|
||||
const curIndex = isNumber(props.modelValue)
|
||||
? toRef(props, "modelValue")
|
||||
: ref(0);
|
||||
|
||||
function handleChange({ option, index }, event: Event) {
|
||||
if (option.disabled) return;
|
||||
event.preventDefault();
|
||||
isNumber(props.modelValue)
|
||||
? emit("update:modelValue", index)
|
||||
: (curIndex.value = index);
|
||||
segmentedItembg.value = "";
|
||||
emit("change", { index, option });
|
||||
}
|
||||
|
||||
function handleMouseenter({ option, index }, event: Event) {
|
||||
event.preventDefault();
|
||||
curMouseActive.value = index;
|
||||
if (option.disabled || curIndex.value === index) {
|
||||
segmentedItembg.value = "";
|
||||
} else {
|
||||
segmentedItembg.value = isDark.value
|
||||
? "#1f1f1f"
|
||||
: "rgba(0, 0, 0, 0.06)";
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseleave(_, event: Event) {
|
||||
event.preventDefault();
|
||||
curMouseActive.value = -1;
|
||||
}
|
||||
|
||||
function handleInit(index = curIndex.value) {
|
||||
nextTick(() => {
|
||||
const curLabelRef = instance?.proxy?.$refs[`labelRef${index}`] as ElRef;
|
||||
width.value = curLabelRef.clientWidth;
|
||||
translateX.value = curLabelRef.offsetLeft;
|
||||
initStatus.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => curIndex.value,
|
||||
index => {
|
||||
nextTick(() => {
|
||||
handleInit(index);
|
||||
});
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
const rendLabel = () => {
|
||||
return props.options.map((option, index) => {
|
||||
return (
|
||||
<label
|
||||
ref={`labelRef${index}`}
|
||||
class={[
|
||||
"pure-segmented-item",
|
||||
option?.disabled && "pure-segmented-item-disabled"
|
||||
]}
|
||||
style={{
|
||||
background:
|
||||
curMouseActive.value === index ? segmentedItembg.value : "",
|
||||
color:
|
||||
!option.disabled &&
|
||||
(curIndex.value === index || curMouseActive.value === index)
|
||||
? isDark.value
|
||||
? "rgba(255, 255, 255, 0.85)"
|
||||
: "rgba(0,0,0,.88)"
|
||||
: ""
|
||||
}}
|
||||
onMouseenter={event => handleMouseenter({ option, index }, event)}
|
||||
onMouseleave={event => handleMouseleave({ option, index }, event)}
|
||||
onClick={event => handleChange({ option, index }, event)}
|
||||
>
|
||||
<input type="radio" name="segmented" />
|
||||
<div
|
||||
class="pure-segmented-item-label"
|
||||
v-tippy={{
|
||||
content: option?.tip,
|
||||
zIndex: 41000
|
||||
}}
|
||||
>
|
||||
{option.icon && !isFunction(option.label) ? (
|
||||
<span
|
||||
class="pure-segmented-item-icon"
|
||||
style={{ marginRight: option.label ? "6px" : 0 }}
|
||||
>
|
||||
{h(
|
||||
useRenderIcon(option.icon, {
|
||||
...option?.iconAttrs
|
||||
})
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{option.label ? (
|
||||
isFunction(option.label) ? (
|
||||
h(option.label)
|
||||
) : (
|
||||
<span>{option.label}</span>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return () => (
|
||||
<div class="pure-segmented">
|
||||
<div class="pure-segmented-group">
|
||||
<div
|
||||
class="pure-segmented-item-selected"
|
||||
style={{
|
||||
width: `${width.value}px`,
|
||||
transform: `translateX(${translateX.value}px)`,
|
||||
display: initStatus.value ? "block" : "none"
|
||||
}}
|
||||
></div>
|
||||
{rendLabel()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
20
src/components/ReSegmented/src/type.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { VNode, Component } from "vue";
|
||||
import type { iconType } from "@/components/ReIcon/src/types.ts";
|
||||
|
||||
export interface OptionsType {
|
||||
/** 文字 */
|
||||
label?: string | (() => VNode | Component);
|
||||
/**
|
||||
* @description 图标,采用平台内置的 `useRenderIcon` 函数渲染
|
||||
* @see {@link 用法参考 https://yiming_chang.gitee.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
|
||||
*/
|
||||
icon?: string | Component;
|
||||
/** 图标属性、样式配置 */
|
||||
iconAttrs?: iconType;
|
||||
/** 值 */
|
||||
value?: any;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** `tooltip` 提示 */
|
||||
tip?: string;
|
||||
}
|
7
src/components/ReText/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import reText from "./src/index.vue";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
|
||||
/** 支持`Tooltip`提示的文本省略组件 */
|
||||
export const ReText = withInstall(reText);
|
||||
|
||||
export default ReText;
|
62
src/components/ReText/src/index.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, onMounted, ref, useSlots } from "vue";
|
||||
import { useTippy, type TippyOptions } from "vue-tippy";
|
||||
|
||||
const props = defineProps({
|
||||
// 行数
|
||||
lineClamp: {
|
||||
type: [String, Number]
|
||||
},
|
||||
tippyProps: {
|
||||
type: Object as PropType<TippyOptions>,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const $slots = useSlots();
|
||||
|
||||
const textRef = ref();
|
||||
const tippyFunc = ref();
|
||||
|
||||
const isTextEllipsis = (el: HTMLElement) => {
|
||||
if (!props.lineClamp) {
|
||||
// 单行省略判断
|
||||
return el.scrollWidth > el.clientWidth;
|
||||
} else {
|
||||
// 多行省略判断
|
||||
return el.scrollHeight > el.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const getTippyProps = () => ({
|
||||
content: h($slots.content || $slots.default),
|
||||
...props.tippyProps
|
||||
});
|
||||
|
||||
function handleHover(event: MouseEvent) {
|
||||
if (isTextEllipsis(event.target as HTMLElement)) {
|
||||
tippyFunc.value.setProps(getTippyProps());
|
||||
tippyFunc.value.enable();
|
||||
} else {
|
||||
tippyFunc.value.disable();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tippyFunc.value = useTippy(textRef.value?.$el, getTippyProps());
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-text
|
||||
v-bind="{
|
||||
truncated: !lineClamp,
|
||||
lineClamp,
|
||||
...$attrs
|
||||
}"
|
||||
ref="textRef"
|
||||
@mouseover.self="handleHover"
|
||||
>
|
||||
<slot />
|
||||
</el-text>
|
||||
</template>
|