✨ refine MyUI
This commit is contained in:
55
components.d.ts
vendored
55
components.d.ts
vendored
@@ -23,6 +23,7 @@ declare module 'vue' {
|
|||||||
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
|
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
|
||||||
AInput: typeof import('ant-design-vue/es')['Input']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||||
|
Alert: typeof import('./src/components/MyUI/Alert/Alert.vue')['default']
|
||||||
AList: typeof import('ant-design-vue/es')['List']
|
AList: typeof import('ant-design-vue/es')['List']
|
||||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||||
@@ -42,47 +43,101 @@ declare module 'vue' {
|
|||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
|
Avatar: typeof import('./src/components/MyUI/Avatar/Avatar.vue')['default']
|
||||||
|
BackTop: typeof import('./src/components/MyUI/BackTop/BackTop.vue')['default']
|
||||||
Badge: typeof import('./src/components/MyUI/Badge/Badge.vue')['default']
|
Badge: typeof import('./src/components/MyUI/Badge/Badge.vue')['default']
|
||||||
BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default']
|
BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default']
|
||||||
|
Breadcrumb: typeof import('./src/components/MyUI/Breadcrumb/Breadcrumb.vue')['default']
|
||||||
|
Button: typeof import('./src/components/MyUI/Button/Button.vue')['default']
|
||||||
|
Card: typeof import('./src/components/MyUI/Card/Card.vue')['default']
|
||||||
Card3D: typeof import('./src/components/Card3D/Card3D.vue')['default']
|
Card3D: typeof import('./src/components/Card3D/Card3D.vue')['default']
|
||||||
|
Carousel: typeof import('./src/components/MyUI/Carousel/Carousel.vue')['default']
|
||||||
|
Cascader: typeof import('./src/components/MyUI/Cascader/Cascader.vue')['default']
|
||||||
|
Checkbox: typeof import('./src/components/MyUI/Checkbox/Checkbox.vue')['default']
|
||||||
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
||||||
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
|
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
|
||||||
|
Col: typeof import('./src/components/MyUI/Grid/Col.vue')['default']
|
||||||
|
Collapse: typeof import('./src/components/MyUI/Collapse/Collapse.vue')['default']
|
||||||
CommentInput: typeof import('./src/components/CommentReply/src/CommentInput/CommentInput.vue')['default']
|
CommentInput: typeof import('./src/components/CommentReply/src/CommentInput/CommentInput.vue')['default']
|
||||||
CommentList: typeof import('./src/components/CommentReply/src/CommentList/CommentList.vue')['default']
|
CommentList: typeof import('./src/components/CommentReply/src/CommentList/CommentList.vue')['default']
|
||||||
CommentReply: typeof import('./src/components/CommentReply/index.vue')['default']
|
CommentReply: typeof import('./src/components/CommentReply/index.vue')['default']
|
||||||
|
Countdown: typeof import('./src/components/MyUI/Countdown/Countdown.vue')['default']
|
||||||
|
DatePicker: typeof import('./src/components/MyUI/DatePicker/DatePicker.vue')['default']
|
||||||
|
Descriptions: typeof import('./src/components/MyUI/Descriptions/Descriptions.vue')['default']
|
||||||
|
Dialog: typeof import('./src/components/MyUI/Dialog/Dialog.vue')['default']
|
||||||
|
Divider: typeof import('./src/components/MyUI/Divider/Divider.vue')['default']
|
||||||
|
Drawer: typeof import('./src/components/MyUI/Drawer/Drawer.vue')['default']
|
||||||
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
|
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
|
||||||
EffectsCard: typeof import('./src/components/EffectsCard/EffectsCard.vue')['default']
|
EffectsCard: typeof import('./src/components/EffectsCard/EffectsCard.vue')['default']
|
||||||
Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.vue')['default']
|
Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.vue')['default']
|
||||||
|
Empty: typeof import('./src/components/MyUI/Empty/Empty.vue')['default']
|
||||||
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
||||||
|
Flex: typeof import('./src/components/MyUI/Flex/Flex.vue')['default']
|
||||||
|
FloatButton: typeof import('./src/components/MyUI/FloatButton/FloatButton.vue')['default']
|
||||||
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
|
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
|
||||||
|
GaugeChart: typeof import('./src/components/MyUI/GaugeChart/GaugeChart.vue')['default']
|
||||||
GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined']
|
GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined']
|
||||||
GradientText: typeof import('./src/components/MyUI/GradientText/GradientText.vue')['default']
|
GradientText: typeof import('./src/components/MyUI/GradientText/GradientText.vue')['default']
|
||||||
|
Image: typeof import('./src/components/MyUI/Image/Image.vue')['default']
|
||||||
|
Input: typeof import('./src/components/MyUI/Input/Input.vue')['default']
|
||||||
|
InputSearch: typeof import('./src/components/MyUI/InputSearch/InputSearch.vue')['default']
|
||||||
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
|
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
|
||||||
|
List: typeof import('./src/components/MyUI/List/List.vue')['default']
|
||||||
|
LoadingBar: typeof import('./src/components/MyUI/LoadingBar/LoadingBar.vue')['default']
|
||||||
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
|
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
|
||||||
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
|
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
|
||||||
LoginPage: typeof import('./src/views/Login/LoginPage.vue')['default']
|
LoginPage: typeof import('./src/views/Login/LoginPage.vue')['default']
|
||||||
MainPage: typeof import('./src/views/Main/MainPage.vue')['default']
|
MainPage: typeof import('./src/views/Main/MainPage.vue')['default']
|
||||||
|
Message: typeof import('./src/components/MyUI/Message/Message.vue')['default']
|
||||||
MessageReport: typeof import('./src/components/CommentReply/src/MessageReport/MessageReport.vue')['default']
|
MessageReport: typeof import('./src/components/CommentReply/src/MessageReport/MessageReport.vue')['default']
|
||||||
|
Modal: typeof import('./src/components/MyUI/Modal/Modal.vue')['default']
|
||||||
NotFound: typeof import('./src/views/404/NotFound.vue')['default']
|
NotFound: typeof import('./src/views/404/NotFound.vue')['default']
|
||||||
|
Notification: typeof import('./src/components/MyUI/Notification/Notification.vue')['default']
|
||||||
|
NumberAnimation: typeof import('./src/components/MyUI/NumberAnimation/NumberAnimation.vue')['default']
|
||||||
|
Pagination: typeof import('./src/components/MyUI/Pagination/Pagination.vue')['default']
|
||||||
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
|
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
|
||||||
|
Popconfirm: typeof import('./src/components/MyUI/Popconfirm/Popconfirm.vue')['default']
|
||||||
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
|
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
|
||||||
|
Progress: typeof import('./src/components/MyUI/Progress/Progress.vue')['default']
|
||||||
QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined']
|
QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined']
|
||||||
|
QRCode: typeof import('./src/components/MyUI/QRCode/QRCode.vue')['default']
|
||||||
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
|
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
|
||||||
QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default']
|
QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default']
|
||||||
|
Radio: typeof import('./src/components/MyUI/Radio/Radio.vue')['default']
|
||||||
|
Rate: typeof import('./src/components/MyUI/Rate/Rate.vue')['default']
|
||||||
ReplyInput: typeof import('./src/components/CommentReply/src/ReplyInput/ReplyInput.vue')['default']
|
ReplyInput: typeof import('./src/components/CommentReply/src/ReplyInput/ReplyInput.vue')['default']
|
||||||
ReplyList: typeof import('./src/components/CommentReply/src/ReplyList/ReplyList.vue')['default']
|
ReplyList: typeof import('./src/components/CommentReply/src/ReplyList/ReplyList.vue')['default']
|
||||||
ReplyReply: typeof import('./src/components/CommentReply/src/ReplyReplyInput/ReplyReply.vue')['default']
|
ReplyReply: typeof import('./src/components/CommentReply/src/ReplyReplyInput/ReplyReply.vue')['default']
|
||||||
|
Result: typeof import('./src/components/MyUI/Result/Result.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
Row: typeof import('./src/components/MyUI/Grid/Row.vue')['default']
|
||||||
SafetyOutlined: typeof import('@ant-design/icons-vue')['SafetyOutlined']
|
SafetyOutlined: typeof import('@ant-design/icons-vue')['SafetyOutlined']
|
||||||
Scrollbar: typeof import('./src/components/MyUI/Scrollbar/Scrollbar.vue')['default']
|
Scrollbar: typeof import('./src/components/MyUI/Scrollbar/Scrollbar.vue')['default']
|
||||||
|
Segmented: typeof import('./src/components/MyUI/Segmented/Segmented.vue')['default']
|
||||||
|
Select: typeof import('./src/components/MyUI/Select/Select.vue')['default']
|
||||||
SendOutlined: typeof import('@ant-design/icons-vue')['SendOutlined']
|
SendOutlined: typeof import('@ant-design/icons-vue')['SendOutlined']
|
||||||
|
Skeleton: typeof import('./src/components/MyUI/Skeleton/Skeleton.vue')['default']
|
||||||
|
Slider: typeof import('./src/components/MyUI/Slider/Slider.vue')['default']
|
||||||
|
Space: typeof import('./src/components/MyUI/Space/Space.vue')['default']
|
||||||
Spin: typeof import('./src/components/MyUI/Spin/Spin.vue')['default']
|
Spin: typeof import('./src/components/MyUI/Spin/Spin.vue')['default']
|
||||||
|
Statistic: typeof import('./src/components/MyUI/Statistic/Statistic.vue')['default']
|
||||||
|
Steps: typeof import('./src/components/MyUI/Steps/Steps.vue')['default']
|
||||||
|
Swiper: typeof import('./src/components/MyUI/Swiper/Swiper.vue')['default']
|
||||||
|
Switch: typeof import('./src/components/MyUI/Switch/Switch.vue')['default']
|
||||||
|
Table: typeof import('./src/components/MyUI/Table/Table.vue')['default']
|
||||||
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
|
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
|
||||||
|
Tabs: typeof import('./src/components/MyUI/Tabs/Tabs.vue')['default']
|
||||||
|
Tag: typeof import('./src/components/MyUI/Tag/Tag.vue')['default']
|
||||||
Textarea: typeof import('./src/components/MyUI/Textarea/Textarea.vue')['default']
|
Textarea: typeof import('./src/components/MyUI/Textarea/Textarea.vue')['default']
|
||||||
|
TextScroll: typeof import('./src/components/MyUI/TextScroll/TextScroll.vue')['default']
|
||||||
|
Timeline: typeof import('./src/components/MyUI/Timeline/Timeline.vue')['default']
|
||||||
Tooltip: typeof import('./src/components/MyUI/Tooltip/Tooltip.vue')['default']
|
Tooltip: typeof import('./src/components/MyUI/Tooltip/Tooltip.vue')['default']
|
||||||
|
TreeChart: typeof import('./src/components/MyUI/TreeChart/TreeChart.vue')['default']
|
||||||
|
Upload: typeof import('./src/components/MyUI/Upload/Upload.vue')['default']
|
||||||
UserInfoCard: typeof import('./src/components/CommentReply/src/UserInfoCard/UserInfoCard.vue')['default']
|
UserInfoCard: typeof import('./src/components/CommentReply/src/UserInfoCard/UserInfoCard.vue')['default']
|
||||||
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
||||||
|
Video: typeof import('./src/components/MyUI/Video/Video.vue')['default']
|
||||||
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
|
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
|
||||||
Waterfall: typeof import('./src/components/MyUI/Waterfall/Waterfall.vue')['default']
|
Waterfall: typeof import('./src/components/MyUI/Waterfall/Waterfall.vue')['default']
|
||||||
WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined']
|
WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined']
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
"@types/json-stringify-safe": "^5.0.3",
|
"@types/json-stringify-safe": "^5.0.3",
|
||||||
"@types/node": "^22.8.6",
|
"@types/node": "^22.8.6",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
|
"@vuepic/vue-datepicker": "^10.0.0",
|
||||||
"@vueuse/core": "^11.2.0",
|
"@vueuse/core": "^11.2.0",
|
||||||
|
"@vueuse/integrations": "^11.2.0",
|
||||||
"alova": "^3.1.1",
|
"alova": "^3.1.1",
|
||||||
"animejs": "^3.2.2",
|
"animejs": "^3.2.2",
|
||||||
"ant-design-vue": "^4.2.5",
|
"ant-design-vue": "^4.2.5",
|
||||||
@@ -26,6 +28,7 @@
|
|||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
"eslint": "9.13.0",
|
"eslint": "9.13.0",
|
||||||
"go-captcha-vue": "^2",
|
"go-captcha-vue": "^2",
|
||||||
"json-stringify-safe": "^5.0.1",
|
"json-stringify-safe": "^5.0.1",
|
||||||
@@ -35,7 +38,9 @@
|
|||||||
"nsfwjs": "^4.2.0",
|
"nsfwjs": "^4.2.0",
|
||||||
"pinia": "^2.2.5",
|
"pinia": "^2.2.5",
|
||||||
"pinia-plugin-persistedstate": "^4.1.2",
|
"pinia-plugin-persistedstate": "^4.1.2",
|
||||||
|
"qrcode": "^1",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
|
"swiper": "^11.1.14",
|
||||||
"unplugin-auto-import": "^0.18.3",
|
"unplugin-auto-import": "^0.18.3",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-html": "^3.2.2",
|
"vite-plugin-html": "^3.2.2",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
// 评论框边框颜色
|
// 评论框边框颜色
|
||||||
--comment-child-box-border-color: #90d952;
|
--comment-child-box-border-color: #90d952;
|
||||||
|
|
||||||
|
// 白色
|
||||||
|
--white-color: #ffffff;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +38,9 @@
|
|||||||
--comment-report-text-color: #ffffff;
|
--comment-report-text-color: #ffffff;
|
||||||
// 评论框边框颜色
|
// 评论框边框颜色
|
||||||
--comment-child-box-border-color: #ffffff;
|
--comment-child-box-border-color: #ffffff;
|
||||||
|
|
||||||
|
// 白色
|
||||||
|
--white-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<AFlex :vertical="true" class="reply-avatar" v-if="item.avatar">
|
<AFlex :vertical="true" class="reply-avatar" v-if="item.avatar">
|
||||||
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}" @openChange="(open: boolean)=>{
|
<Popover trigger="click" :arrow="false" :offset-x="170" :contentStyle="{padding: 0}" @openChange="(open: boolean)=>{
|
||||||
console.log(open);
|
console.log(open);
|
||||||
}">
|
}">
|
||||||
<template #content>
|
<template #content>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<AFlex :vertical="true" class="reply-content">
|
<AFlex :vertical="true" class="reply-content">
|
||||||
<AFlex :vertical="true">
|
<AFlex :vertical="true">
|
||||||
<AFlex :vertical="false" align="center" justify="flex-start">
|
<AFlex :vertical="false" align="center" justify="flex-start">
|
||||||
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
|
<Popover trigger="click" :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
|
||||||
<template #content>
|
<template #content>
|
||||||
<UserInfoCard :user="item" :padding="0"/>
|
<UserInfoCard :user="item" :padding="0"/>
|
||||||
</template>
|
</template>
|
||||||
@@ -331,7 +331,7 @@ async function getLatestCommentList() {
|
|||||||
query: {
|
query: {
|
||||||
type: "latest",
|
type: "latest",
|
||||||
page: router.currentRoute.value.query.page,
|
page: router.currentRoute.value.query.page,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
comment.commentLoading = false;
|
comment.commentLoading = false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<AFlex :vertical="false" style="margin-top: 5px" v-for="(child, index) in comment.replyList.comments"
|
<AFlex :vertical="false" style="margin-top: 5px" v-for="(child, index) in comment.replyList.comments"
|
||||||
:key="index">
|
:key="index">
|
||||||
<AFlex :vertical="true">
|
<AFlex :vertical="true">
|
||||||
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
|
<Popover trigger="click" :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
|
||||||
<template #content>
|
<template #content>
|
||||||
<UserInfoCard :user="child" :padding="0"/>
|
<UserInfoCard :user="child" :padding="0"/>
|
||||||
</template>
|
</template>
|
||||||
@@ -20,13 +20,13 @@
|
|||||||
<AFlex :vertical="true" class="reply-item-child-content">
|
<AFlex :vertical="true" class="reply-item-child-content">
|
||||||
<AFlex :vertical="true">
|
<AFlex :vertical="true">
|
||||||
<AFlex :vertical="false" align="center">
|
<AFlex :vertical="false" align="center">
|
||||||
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
|
<Popover trigger="click" :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
|
||||||
<template #content>
|
<template #content>
|
||||||
<UserInfoCard :user="child" :padding="0"/>
|
<UserInfoCard :user="child" :padding="0"/>
|
||||||
</template>
|
</template>
|
||||||
<span class="reply-name-child">{{ child.nickname }}</span>
|
<span class="reply-name-child">{{ child.nickname }}</span>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
|
<Popover trigger="click" :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
|
||||||
<template #content>
|
<template #content>
|
||||||
<UserInfoCard :user="child" :padding="0"/>
|
<UserInfoCard :user="child" :padding="0"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</AFlex>
|
</AFlex>
|
||||||
<AFlex :vertical="false" align="center" justify="flex-start">
|
<AFlex :vertical="false" align="center" justify="flex-start">
|
||||||
<AButton type="primary">
|
<AButton type="primary" >
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<PlusOutlined/>
|
<PlusOutlined/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
422
src/components/MyUI/Alert/Alert.vue
Normal file
422
src/components/MyUI/Alert/Alert.vue
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {Slot} from 'vue';
|
||||||
|
import {computed, nextTick, ref} from 'vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message?: string // 警告提示内容 string | slot
|
||||||
|
description?: string // 警告提示的辅助性文字介绍 string | slot
|
||||||
|
type?: 'default' | 'success' | 'info' | 'warning' | 'error' // 警告提示的类型
|
||||||
|
bordered?: boolean // 是否显示边框
|
||||||
|
closable?: boolean // 是否显示关闭按钮
|
||||||
|
closeText?: string // 自定义关闭按钮 string | slot
|
||||||
|
icon?: string // 自定义图标,showIcon 为 true 时有效 string | slot
|
||||||
|
showIcon?: boolean // 是否显示辅助图标
|
||||||
|
actions?: Slot // 自定义操作项 slot
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
message: undefined,
|
||||||
|
description: undefined,
|
||||||
|
type: 'default',
|
||||||
|
bordered: true,
|
||||||
|
closable: false,
|
||||||
|
closeText: undefined,
|
||||||
|
icon: undefined,
|
||||||
|
showIcon: false,
|
||||||
|
actions: undefined
|
||||||
|
});
|
||||||
|
const alertRef = ref(); // alert 模板引用
|
||||||
|
const closeAlert = ref(false);
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
const slotsExist = useSlotsExist(['icon', 'description', 'actions']);
|
||||||
|
const showSlotsIcon = computed(() => {
|
||||||
|
return slotsExist.icon || props.icon || ['success', 'info', 'warning', 'error'].includes(props.type);
|
||||||
|
});
|
||||||
|
const showDesc = computed(() => {
|
||||||
|
return slotsExist.description || props.description;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onClose(e: Event) {
|
||||||
|
alertRef.value.style.maxHeight = `${alertRef.value.offsetHeight}px`;
|
||||||
|
await nextTick();
|
||||||
|
closeAlert.value = true;
|
||||||
|
emit('close', e);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Transition name="alert-motion">
|
||||||
|
<div
|
||||||
|
v-if="!closeAlert"
|
||||||
|
ref="alertRef"
|
||||||
|
class="m-alert"
|
||||||
|
:class="[
|
||||||
|
`alert-${type}`,
|
||||||
|
{
|
||||||
|
'alert-borderless': !bordered,
|
||||||
|
'alert-width-description': showDesc
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template v-if="showIcon && showSlotsIcon">
|
||||||
|
<span v-if="!showDesc" class="m-alert-icon">
|
||||||
|
<slot name="icon">
|
||||||
|
<img v-if="icon" :src="icon" class="icon-img"/>
|
||||||
|
<svg
|
||||||
|
v-else-if="type === 'info'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="info-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="type === 'success'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="check-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="type === 'warning'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="exclamation-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="type === 'error'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
<span v-else class="m-big-icon">
|
||||||
|
<slot name="icon">
|
||||||
|
<img v-if="icon" :src="icon" class="big-icon-img"/>
|
||||||
|
<svg
|
||||||
|
v-else-if="type === 'info'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="info-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="type === 'success'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="check-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="type === 'warning'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="exclamation-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M464 688a48 48 0 1096 0 48 48 0 10-96 0zm24-112h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="type === 'error'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M685.4 354.8c0-4.4-3.6-8-8-8l-66 .3L512 465.6l-99.3-118.4-66.1-.3c-4.4 0-8 3.5-8 8 0 1.9.7 3.7 1.9 5.2l130.1 155L340.5 670a8.32 8.32 0 00-1.9 5.2c0 4.4 3.6 8 8 8l66.1-.3L512 564.4l99.3 118.4 66 .3c4.4 0 8-3.5 8-8 0-1.9-.7-3.7-1.9-5.2L553.5 515l130.1-155c1.2-1.4 1.8-3.3 1.8-5.2z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M512 65C264.6 65 64 265.6 64 513s200.6 448 448 448 448-200.6 448-448S759.4 65 512 65zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="m-alert-content">
|
||||||
|
<div class="alert-message">
|
||||||
|
<slot>{{ message }}</slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="showDesc" class="alert-description">
|
||||||
|
<slot name="description">{{ description }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="slotsExist.actions" class="m-alert-actions">
|
||||||
|
<slot name="actions"></slot>
|
||||||
|
</div>
|
||||||
|
<a v-if="closable" tabindex="0" class="m-alert-close" @click="onClose" @keydown.enter.prevent="onClose">
|
||||||
|
<slot name="closeText">
|
||||||
|
<span v-if="closeText">{{ closeText }}</span>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="alert-close"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.alert-motion-enter-active,
|
||||||
|
.alert-motion-leave-active {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
|
||||||
|
opacity 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
|
||||||
|
padding 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-motion-leave-to {
|
||||||
|
max-height: 0 !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
padding-block: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-alert {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
word-break: break-all;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.m-alert-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-big-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-img {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-icon-img {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-svg {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-alert-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-alert-actions {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-alert-close {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
line-height: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-close {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
fill: rgba(0, 0, 0, 0.45);
|
||||||
|
transition: fill 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-default {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
|
||||||
|
.m-alert-icon,
|
||||||
|
.m-big-icon {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
|
||||||
|
.icon-svg,
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: #e6f4ff;
|
||||||
|
border: 1px solid #91caff;
|
||||||
|
|
||||||
|
.m-alert-icon,
|
||||||
|
.m-big-icon {
|
||||||
|
color: @themeColor;
|
||||||
|
|
||||||
|
.icon-svg,
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #f6ffed;
|
||||||
|
border: 1px solid #b7eb8f;
|
||||||
|
|
||||||
|
.m-alert-icon,
|
||||||
|
.m-big-icon {
|
||||||
|
color: #52c41a;
|
||||||
|
|
||||||
|
.icon-svg,
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: #fffbe6;
|
||||||
|
border: 1px solid #ffe58f;
|
||||||
|
|
||||||
|
.m-alert-icon,
|
||||||
|
.m-big-icon {
|
||||||
|
color: #faad14;
|
||||||
|
|
||||||
|
.icon-svg,
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: #fff2f0;
|
||||||
|
border: 1px solid #ffccc7;
|
||||||
|
|
||||||
|
.m-alert-icon,
|
||||||
|
.m-big-icon {
|
||||||
|
color: #ff4d4f;
|
||||||
|
|
||||||
|
.icon-svg,
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-borderless {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-width-description {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px 24px;
|
||||||
|
|
||||||
|
.alert-message {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-description {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
216
src/components/MyUI/Avatar/Avatar.vue
Normal file
216
src/components/MyUI/Avatar/Avatar.vue
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import type { VNode, Slot } from 'vue';
|
||||||
|
import { useEventListener, useSlotsExist } from '../Utils';
|
||||||
|
interface Responsive {
|
||||||
|
xs?: number // <576px 响应式栅格
|
||||||
|
sm?: number // ≥576px 响应式栅格
|
||||||
|
md?: number // ≥768px 响应式栅格
|
||||||
|
lg?: number // ≥992px 响应式栅格
|
||||||
|
xl?: number // ≥1200px 响应式栅格
|
||||||
|
xxl?: number // ≥1600px 响应式栅格
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
color?: string // 头像的背景色
|
||||||
|
shape?: 'circle' | 'square' // 指定头像的形状
|
||||||
|
size?: number | 'small' | 'middle' | 'large' | Responsive // 设置头像的大小,number 类型时单位 px
|
||||||
|
src?: string // 图片类头像资源地址
|
||||||
|
alt?: string // 图片无法显示时的替代文本
|
||||||
|
icon?: VNode | Slot // 设置头像的图标
|
||||||
|
href?: string // 点击跳转的地址,指定此属性按钮的行为和 a 链接一致
|
||||||
|
target?: '_self' | '_blank' // 相当于 a 标签的 target 属性,href 存在时生效
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
color: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
shape: 'circle',
|
||||||
|
size: 'middle',
|
||||||
|
src: undefined,
|
||||||
|
alt: undefined,
|
||||||
|
icon: undefined,
|
||||||
|
href: undefined,
|
||||||
|
target: '_self'
|
||||||
|
});
|
||||||
|
const viewportWidth = ref(window.innerWidth);
|
||||||
|
function getViewportWidth() {
|
||||||
|
viewportWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
useEventListener(window, 'resize', getViewportWidth);
|
||||||
|
const slotsExist = useSlotsExist(['default', 'icon']);
|
||||||
|
const showIcon = computed(() => {
|
||||||
|
if (!props.src) {
|
||||||
|
return Boolean(slotsExist.icon || props.icon);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const avatarStyle = computed(() => {
|
||||||
|
if (typeof props.size === 'number') {
|
||||||
|
if (showIcon.value) {
|
||||||
|
return {
|
||||||
|
backgroundColor: props.color,
|
||||||
|
width: `${props.size}px`,
|
||||||
|
height: `${props.size}px`,
|
||||||
|
lineHeight: `${props.size}px`,
|
||||||
|
fontSize: `${props.size / 2}px`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
backgroundColor: props.color,
|
||||||
|
width: `${props.size}px`,
|
||||||
|
height: `${props.size}px`,
|
||||||
|
lineHeight: `${props.size}px`,
|
||||||
|
fontSize: '18px'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof props.size === 'object') {
|
||||||
|
let size = 32;
|
||||||
|
if (viewportWidth.value >= 1600 && props.size.xxl) {
|
||||||
|
size = props.size.xxl;
|
||||||
|
} else if (viewportWidth.value >= 1200 && props.size.xl) {
|
||||||
|
size = props.size.xl;
|
||||||
|
} else if (viewportWidth.value >= 992 && props.size.lg) {
|
||||||
|
size = props.size.lg;
|
||||||
|
} else if (viewportWidth.value >= 768 && props.size.md) {
|
||||||
|
size = props.size.md;
|
||||||
|
} else if (viewportWidth.value >= 576 && props.size.sm) {
|
||||||
|
size = props.size.sm;
|
||||||
|
} else if (viewportWidth.value < 576 && props.size.xs) {
|
||||||
|
size = props.size.xs;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
backgroundColor: props.color,
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
lineHeight: `${size}px`,
|
||||||
|
fontSize: `${size / 2}px`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
backgroundColor: props.color
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const showStr = computed(() => {
|
||||||
|
if (!props.src && !showIcon.value) {
|
||||||
|
return slotsExist.default;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const strStyle = computed(() => {
|
||||||
|
if (typeof props.size === 'string') {
|
||||||
|
return {
|
||||||
|
transform: 'scale(1) translateX(-50%)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof props.size === 'number') {
|
||||||
|
const scale = Math.min(1, Math.max(1 / 45, (1 + (props.size - 9) * 1) / 45));
|
||||||
|
return {
|
||||||
|
lineHeight: `${props.size}px`,
|
||||||
|
transform: `scale(${scale}) translateX(-50%)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="href ? 'a' : 'div'"
|
||||||
|
class="m-avatar"
|
||||||
|
:class="[
|
||||||
|
`avatar-${shape}`,
|
||||||
|
{
|
||||||
|
[`avatar-${size}`]: typeof size === 'string' && ['small', 'middle', 'large'].includes(size),
|
||||||
|
'avatar-image': src,
|
||||||
|
'avatar-link': href
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:style="avatarStyle"
|
||||||
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
>
|
||||||
|
<img v-if="src" class="image-item" :src="src" :alt="alt" />
|
||||||
|
<slot v-if="!src && showIcon" name="icon">
|
||||||
|
<component :is="icon" />
|
||||||
|
</slot>
|
||||||
|
<span v-if="!src && !showIcon && showStr" class="string-item" :style="strStyle">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-avatar {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 30px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: auto;
|
||||||
|
outline: none;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&.avatar-square {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.image-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
:deep(svg) {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
.string-item {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform-origin: 0 center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-small {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
.avatar-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
&.avatar-square {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-middle {
|
||||||
|
.avatar-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-large {
|
||||||
|
font-size: 24px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
.avatar-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
&.avatar-square {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-image {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.avatar-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
311
src/components/MyUI/BackTop/BackTop.vue
Normal file
311
src/components/MyUI/BackTop/BackTop.vue
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {Slot, VNode} from 'vue';
|
||||||
|
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
||||||
|
import Tooltip from '../Tooltip/Tooltip.vue';
|
||||||
|
import {useMutationObserver, useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: VNode | Slot // 自定义图标
|
||||||
|
description?: string // 文字描述 string | slot
|
||||||
|
tooltip?: string // 文字提示内容 string | slot
|
||||||
|
tooltipProps?: object // Tooltip 组件属性配置,参考 Tooltip Props
|
||||||
|
type?: 'default' | 'primary' // 设置按钮类型
|
||||||
|
shape?: 'circle' | 'square' // 设置按钮形状
|
||||||
|
bottom?: number | string // BackTop 距离页面底部的高度,单位 px
|
||||||
|
right?: number | string // BackTop 距离页面右侧的宽度,单位 px
|
||||||
|
zIndex?: number // 设置 BackTop 的 z-index
|
||||||
|
visibilityHeight?: number // 滚动时触发显示回到顶部按钮的高度,单位 px
|
||||||
|
to?: string | HTMLElement // BackTop 渲染的容器节点,可选:元素标签名 (例如 'body') 或者元素本身,下同
|
||||||
|
listenTo?: string | HTMLElement // 监听滚动的元素,如果为 undefined 会监听距离最近的一个可滚动的祖先节点
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
icon: undefined,
|
||||||
|
description: undefined,
|
||||||
|
tooltip: undefined,
|
||||||
|
tooltipProps: () => ({}),
|
||||||
|
type: 'default',
|
||||||
|
shape: 'circle',
|
||||||
|
bottom: 40,
|
||||||
|
right: 40,
|
||||||
|
zIndex: 9,
|
||||||
|
visibilityHeight: 180,
|
||||||
|
to: 'body',
|
||||||
|
listenTo: undefined
|
||||||
|
});
|
||||||
|
const backtopRef = ref<HTMLElement | null>(null);
|
||||||
|
const scrollTop = ref<number>(0); // 滚动距离
|
||||||
|
const scrollTarget = ref<HTMLElement | null>(null); // 滚动目标元素
|
||||||
|
const targetElement = ref<HTMLElement | null>(null); // 渲染容器元素
|
||||||
|
const emits = defineEmits(['click', 'show']);
|
||||||
|
const slotsExist = useSlotsExist(['tooltip', 'icon', 'description']);
|
||||||
|
const backTopStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
bottom: typeof props.bottom === 'number' ? `${props.bottom}px` : props.bottom,
|
||||||
|
right: typeof props.right === 'number' ? `${props.right}px` : props.right,
|
||||||
|
zIndex: props.zIndex
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const backTopShow = computed(() => {
|
||||||
|
return scrollTop.value >= props.visibilityHeight;
|
||||||
|
});
|
||||||
|
const showTooltip = computed(() => {
|
||||||
|
return slotsExist.tooltip || props.tooltip;
|
||||||
|
});
|
||||||
|
const showDescription = computed(() => {
|
||||||
|
return slotsExist.description || props.description;
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.to,
|
||||||
|
() => {
|
||||||
|
appendBackTop();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flush: 'post'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.listenTo,
|
||||||
|
() => {
|
||||||
|
observeScroll();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flush: 'post'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(backTopShow, (to) => {
|
||||||
|
emits('show', to);
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
observeScroll();
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cleanup();
|
||||||
|
backtopRef.value?.remove();
|
||||||
|
});
|
||||||
|
const mutationObserver = useMutationObserver(
|
||||||
|
scrollTarget,
|
||||||
|
() => {
|
||||||
|
scrollTop.value = scrollTarget.value?.scrollTop ?? 0;
|
||||||
|
},
|
||||||
|
{subtree: true, childList: true, attributes: true, characterData: true}
|
||||||
|
);
|
||||||
|
|
||||||
|
function updateScrollTop(e: Event) {
|
||||||
|
scrollTop.value = (e.target as HTMLElement).scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询并监听滚动元素
|
||||||
|
function observeScroll() {
|
||||||
|
cleanup();
|
||||||
|
if (props.listenTo === undefined) {
|
||||||
|
scrollTarget.value = getScrollParent(backtopRef.value?.parentElement ?? null);
|
||||||
|
} else if (typeof props.listenTo === 'string') {
|
||||||
|
scrollTarget.value = document.getElementsByTagName(props.listenTo)[0] as HTMLElement;
|
||||||
|
} else if (props.listenTo instanceof HTMLElement) {
|
||||||
|
scrollTarget.value = props.listenTo;
|
||||||
|
}
|
||||||
|
if (scrollTarget.value) {
|
||||||
|
scrollTarget.value.addEventListener('scroll', updateScrollTop);
|
||||||
|
}
|
||||||
|
if (scrollTarget.value === document.documentElement) {
|
||||||
|
mutationObserver.start();
|
||||||
|
}
|
||||||
|
appendBackTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (scrollTarget.value) {
|
||||||
|
scrollTarget.value.removeEventListener('scroll', updateScrollTop);
|
||||||
|
}
|
||||||
|
scrollTarget.value = null;
|
||||||
|
mutationObserver.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendBackTop() {
|
||||||
|
// 渲染容器节点
|
||||||
|
if (typeof props.to === 'string') {
|
||||||
|
targetElement.value = document.getElementsByTagName(props.to)[0] as HTMLElement;
|
||||||
|
} else if (props.to instanceof HTMLElement) {
|
||||||
|
targetElement.value = props.to;
|
||||||
|
}
|
||||||
|
if (targetElement.value) {
|
||||||
|
targetElement.value?.appendChild(backtopRef.value!); // 保证 backtop 节点只存在一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScrollParent(el: HTMLElement | null): HTMLElement | null {
|
||||||
|
const isScrollable = (el: HTMLElement): boolean => {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
if (
|
||||||
|
el.scrollHeight > el.clientHeight &&
|
||||||
|
(['scroll', 'auto'].includes(style.overflowY) || el === document.documentElement)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (el) {
|
||||||
|
return isScrollable(el) ? el : getScrollParent(el.parentElement ?? null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackTop() {
|
||||||
|
|
||||||
|
if (scrollTarget.value) {
|
||||||
|
scrollTarget.value.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth' // 平滑滚动并产生过渡效果
|
||||||
|
});
|
||||||
|
}
|
||||||
|
emits('click');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Transition name="zoom">
|
||||||
|
<div v-show="backTopShow" ref="backtopRef" class="m-backtop-wrap" :style="backTopStyle" @click="onBackTop">
|
||||||
|
<Tooltip style="border-radius: 22px" :content-style="{ borderRadius: '22px' }" v-bind="tooltipProps">
|
||||||
|
<template v-if="showTooltip" #tooltip>
|
||||||
|
<slot name="tooltip">{{ tooltip }}</slot>
|
||||||
|
</template>
|
||||||
|
<div class="m-backtop" :class="`backtop-${type} backtop-${shape}`">
|
||||||
|
<slot>
|
||||||
|
<span class="backtop-icon" :class="{ 'icon-description': showDescription }">
|
||||||
|
<slot name="icon">
|
||||||
|
<component v-if="icon" :is="icon"/>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xlinkHref="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||||
|
<g transform="translate(-139.000000, -4423.000000)" fill-rule="nonzero">
|
||||||
|
<g transform="translate(120.000000, 4285.000000)">
|
||||||
|
<g transform="translate(7.000000, 126.000000)">
|
||||||
|
<g
|
||||||
|
transform="translate(24.000000, 24.000000) scale(1, -1) translate(-24.000000, -24.000000) translate(12.000000, 12.000000)"
|
||||||
|
>
|
||||||
|
<g transform="translate(4.000000, 2.000000)">
|
||||||
|
<path
|
||||||
|
d="M8,0 C8.51283584,0 8.93550716,0.38604019 8.99327227,0.883378875 L9,1 L9,10.584 L12.2928932,7.29289322 C12.6834175,6.90236893 13.3165825,6.90236893 13.7071068,7.29289322 C14.0675907,7.65337718 14.0953203,8.22060824 13.7902954,8.61289944 L13.7071068,8.70710678 L8.70710678,13.7071068 L8.62544899,13.7803112 L8.618,13.784 L8.59530661,13.8036654 L8.4840621,13.8753288 L8.37133602,13.9287745 L8.22929083,13.9735893 L8.14346259,13.9897165 L8.03324678,13.9994506 L7.9137692,13.9962979 L7.77070917,13.9735893 L7.6583843,13.9401293 L7.57677845,13.9063266 L7.47929125,13.8540045 L7.4048407,13.8036865 L7.38131006,13.7856883 C7.35030318,13.7612383 7.32077858,13.7349921 7.29289322,13.7071068 L2.29289322,8.70710678 L2.20970461,8.61289944 C1.90467972,8.22060824 1.93240926,7.65337718 2.29289322,7.29289322 C2.65337718,6.93240926 3.22060824,6.90467972 3.61289944,7.20970461 L3.70710678,7.29289322 L7,10.585 L7,1 L7.00672773,0.883378875 C7.06449284,0.38604019 7.48716416,0 8,0 Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M14.9333333,15.9994506 C15.5224371,15.9994506 16,16.4471659 16,16.9994506 C16,17.5122865 15.5882238,17.9349578 15.0577292,17.9927229 L14.9333333,17.9994506 L1.06666667,17.9994506 C0.477562934,17.9994506 0,17.5517354 0,16.9994506 C0,16.4866148 0.411776203,16.0639435 0.9422708,16.0061783 L1.06666667,15.9994506 L14.9333333,15.9994506 Z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
<span v-if="showDescription" class="backtop-description">
|
||||||
|
<slot name="description">{{ description }}</slot>
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.zoom-enter-active,
|
||||||
|
.zoom-leave-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-enter-from,
|
||||||
|
.zoom-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-backtop-wrap {
|
||||||
|
position: fixed;
|
||||||
|
z-index: var(--z-index);
|
||||||
|
|
||||||
|
.m-backtop {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
.backtop-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 26px;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
pointer-events: none;
|
||||||
|
fill: currentColor;
|
||||||
|
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-description {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtop-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtop-default {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
background-color: rgba(255, 255, 255, 0.88);
|
||||||
|
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
.backtop-icon,
|
||||||
|
.backtop-description {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255);
|
||||||
|
box-shadow: 0 2px 8px 3px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
.backtop-icon,
|
||||||
|
.backtop-description {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtop-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #4096ff;
|
||||||
|
box-shadow: 0 2px 8px 0 rgba(9, 88, 217, 0.32);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #4096ff;
|
||||||
|
box-shadow: 0 2px 8px 3px rgba(9, 88, 217, 0.32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtop-circle {
|
||||||
|
border-radius: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtop-square {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import { useSlotsExist } from '../utils/index.ts';
|
import { useSlotsExist } from '../Utils/index.ts';
|
||||||
enum PresetColor {
|
enum PresetColor {
|
||||||
pink = 'pink',
|
pink = 'pink',
|
||||||
red = 'red',
|
red = 'red',
|
||||||
|
|||||||
148
src/components/MyUI/Breadcrumb/Breadcrumb.vue
Normal file
148
src/components/MyUI/Breadcrumb/Breadcrumb.vue
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed} from 'vue';
|
||||||
|
|
||||||
|
interface Query {
|
||||||
|
[propName: string]: any // 添加一个字符串索引签名,用于包含带有任意数量的其他属性
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Route {
|
||||||
|
name: string // 路由名称
|
||||||
|
path?: string // 路由地址
|
||||||
|
query?: Query // 路由查询参数
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
routes?: Route[] // router 路由数组
|
||||||
|
breadcrumbClass?: string // 设置面包屑类名
|
||||||
|
breadcrumbStyle?: CSSProperties // 设置面包屑样式
|
||||||
|
maxWidth?: string | number // 设置文本最大显示宽度,超出后显示省略号,单位 px
|
||||||
|
separator?: string // 自定义分隔符,默认为 > string | slot
|
||||||
|
separatorStyle?: CSSProperties // 设置分隔符样式
|
||||||
|
target?: '_self' | '_blank' // 如何打开目标 URL,当前窗口或新窗口
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
routes: () => [],
|
||||||
|
breadcrumbClass: undefined,
|
||||||
|
breadcrumbStyle: () => ({}),
|
||||||
|
maxWidth: '100%',
|
||||||
|
separator: undefined,
|
||||||
|
separatorStyle: () => ({}),
|
||||||
|
target: '_self'
|
||||||
|
});
|
||||||
|
const breadcrumbAmount = computed(() => {
|
||||||
|
return props.routes.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getUrl(route: Route) {
|
||||||
|
let targetUrl: string = '';
|
||||||
|
if (route.path) {
|
||||||
|
targetUrl = route.path;
|
||||||
|
}
|
||||||
|
if (route.query && JSON.stringify(route.query) !== '{}') {
|
||||||
|
const query = route.query;
|
||||||
|
Object.keys(query).forEach((param, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
targetUrl = targetUrl + '?' + param + '=' + query[param];
|
||||||
|
} else {
|
||||||
|
targetUrl = targetUrl + '&' + param + '=' + query[param];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return targetUrl;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-breadcrumb" :class="breadcrumbClass" :style="breadcrumbStyle">
|
||||||
|
<div class="m-breadcrumb-item" v-for="(route, index) in routes" :key="index">
|
||||||
|
<component
|
||||||
|
:is="route.path ? 'a' : 'span'"
|
||||||
|
class="breadcrumb-link"
|
||||||
|
:class="{
|
||||||
|
'link-hover': route.path,
|
||||||
|
'link-active': index === breadcrumbAmount - 1
|
||||||
|
}"
|
||||||
|
:style="`max-width: ${maxWidth}px;`"
|
||||||
|
:href="getUrl(route)"
|
||||||
|
:target="target"
|
||||||
|
:title="route.name"
|
||||||
|
>
|
||||||
|
{{ route.name }}
|
||||||
|
</component>
|
||||||
|
<span v-if="index < breadcrumbAmount - 1" class="breadcrumb-separator" :style="separatorStyle">
|
||||||
|
<slot name="separator" :index="index">
|
||||||
|
<span v-if="separator">{{ separator }}</span>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
focusable="false"
|
||||||
|
data-icon="right"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-breadcrumb {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.m-breadcrumb-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.breadcrumb-link {
|
||||||
|
display: inline-block;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: text;
|
||||||
|
transition: color 0.2s,
|
||||||
|
background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-active {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-separator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 4px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
margin-inline: -2px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
497
src/components/MyUI/Button/Button.vue
Normal file
497
src/components/MyUI/Button/Button.vue
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, computed } from 'vue';
|
||||||
|
import type { VNode, Slot } from 'vue';
|
||||||
|
import { useSlotsExist } from '../Utils';
|
||||||
|
interface Props {
|
||||||
|
type?: 'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link' // 设置按钮类型
|
||||||
|
shape?: 'default' | 'circle' | 'round' // 设置按钮形状
|
||||||
|
icon?: VNode | Slot // 设置按钮图标
|
||||||
|
size?: 'small' | 'middle' | 'large' // 设置按钮尺寸
|
||||||
|
ghost?: boolean // 按钮背景是否透明,仅当 type: 'primary' | 'danger' 时生效
|
||||||
|
buttonClass?: string // 设置按钮类名
|
||||||
|
rippleColor?: string // 点击时的波纹颜色,一般不需要设置,默认会根据 type 自动匹配,主要用于自定义样式时且 type: 'default'
|
||||||
|
href?: string // 点击跳转的地址,与 a 链接的 href 属性一致
|
||||||
|
target?: '_self' | '_blank' // 如何打开目标链接,相当于 a 链接的 target 属性,href 存在时生效
|
||||||
|
keyboard?: boolean // 是否支持键盘操作
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
loading?: boolean // 是否加载中
|
||||||
|
loadingType?: 'static' | 'dynamic' // 加载指示符类型
|
||||||
|
block?: boolean // 是否将按钮宽度调整为其父宽度
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
type: 'default',
|
||||||
|
shape: 'default',
|
||||||
|
icon: undefined,
|
||||||
|
size: 'middle',
|
||||||
|
ghost: false,
|
||||||
|
rippleColor: undefined,
|
||||||
|
buttonClass: undefined,
|
||||||
|
href: undefined,
|
||||||
|
target: '_self',
|
||||||
|
keyboard: true,
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
loadingType: 'dynamic',
|
||||||
|
block: false
|
||||||
|
});
|
||||||
|
const presetRippleColors = {
|
||||||
|
default: '#1677ff',
|
||||||
|
reverse: '#1677ff',
|
||||||
|
primary: '#1677ff',
|
||||||
|
danger: '#ff4d4f',
|
||||||
|
dashed: '#1677ff',
|
||||||
|
text: 'transparent',
|
||||||
|
link: 'transparent'
|
||||||
|
};
|
||||||
|
const wave = ref(false);
|
||||||
|
const emit = defineEmits(['click']);
|
||||||
|
const slotsExist = useSlotsExist(['icon', 'default']);
|
||||||
|
const showIcon = computed(() => {
|
||||||
|
return slotsExist.icon || props.icon;
|
||||||
|
});
|
||||||
|
const showIconOnly = computed(() => {
|
||||||
|
return showIcon.value && !slotsExist.default;
|
||||||
|
});
|
||||||
|
function onClick(e: Event) {
|
||||||
|
if (wave.value) {
|
||||||
|
wave.value = false;
|
||||||
|
nextTick(() => {
|
||||||
|
wave.value = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wave.value = true;
|
||||||
|
}
|
||||||
|
emit('click', e);
|
||||||
|
}
|
||||||
|
function onKeyboard(e: KeyboardEvent) {
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
function onWaveEnd() {
|
||||||
|
wave.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="href ? 'a' : 'div'"
|
||||||
|
tabindex="0"
|
||||||
|
class="m-btn"
|
||||||
|
:class="[
|
||||||
|
`btn-${type} btn-${size}`,
|
||||||
|
{
|
||||||
|
[`loading-${size}`]: !href && loading,
|
||||||
|
'btn-icon-only': showIconOnly,
|
||||||
|
'btn-circle': shape === 'circle',
|
||||||
|
'btn-round': shape === 'round',
|
||||||
|
'btn-loading-blur': !href && loading,
|
||||||
|
'btn-ghost': ghost,
|
||||||
|
'btn-block': block,
|
||||||
|
'btn-disabled': disabled
|
||||||
|
},
|
||||||
|
buttonClass
|
||||||
|
]"
|
||||||
|
:style="`--ripple-color: ${rippleColor || presetRippleColors[type]};`"
|
||||||
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
@click="disabled || loading ? () => false : onClick($event)"
|
||||||
|
@keydown.enter.prevent="keyboard && !disabled && !loading ? onKeyboard($event) : () => false"
|
||||||
|
>
|
||||||
|
<div v-if="loading || !showIcon" class="btn-loading">
|
||||||
|
<div v-if="!href && loadingType === 'static'" class="m-static-circle">
|
||||||
|
<svg class="circle" width="1em" height="1em" fill="currentColor" viewBox="0 0 100 100">
|
||||||
|
<path
|
||||||
|
d="M 50,50 m 0,-45 a 45,45 0 1 1 0,90 a 45,45 0 1 1 0,-90"
|
||||||
|
stroke-linecap="round"
|
||||||
|
class="path"
|
||||||
|
fill-opacity="0"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div v-if="!href && loadingType === 'dynamic'" class="m-dynamic-circle">
|
||||||
|
<svg class="circle" viewBox="0 0 50 50" width="1em" height="1em" fill="currentColor">
|
||||||
|
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="!loading && showIcon" class="btn-icon">
|
||||||
|
<slot name="icon">
|
||||||
|
<component v-if="icon" :is="icon" />
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
<span v-if="slotsExist.default" class="btn-content">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
<div v-if="!disabled" class="button-wave" :class="{ 'wave-active': wave }" @animationend="onWaveEnd"></div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@primary: #1677ff;
|
||||||
|
@danger: #ff4d4f;
|
||||||
|
.m-btn {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
.btn-loading {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
transition:
|
||||||
|
margin-right 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||||
|
width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||||
|
opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
.m-static-circle,
|
||||||
|
.m-dynamic-circle {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: start;
|
||||||
|
.circle .path {
|
||||||
|
stroke: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.m-static-circle {
|
||||||
|
.circle {
|
||||||
|
animation: spin-circle 0.8s linear infinite;
|
||||||
|
-webkit-animation: spin-circle 0.8s linear infinite;
|
||||||
|
.path {
|
||||||
|
stroke-width: 10;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke-dasharray: 84.82px, 282.74px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.m-dynamic-circle {
|
||||||
|
.circle {
|
||||||
|
animation: spin-circle 2s linear infinite;
|
||||||
|
-webkit-animation: spin-circle 2s linear infinite;
|
||||||
|
.path {
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke-linecap: round;
|
||||||
|
animation: loading-dash 1.5s ease-in-out infinite;
|
||||||
|
-webkit-animation: loading-dash 1.5s ease-in-out infinite;
|
||||||
|
@keyframes loading-dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -40px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -124px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes spin-circle {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-icon,
|
||||||
|
.btn-content {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button-wave {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
animation-duration: 0.6s;
|
||||||
|
animation-timing-function: cubic-bezier(0, 0, 0.2, 1), cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
.wave-active {
|
||||||
|
z-index: 1;
|
||||||
|
animation-name: wave-spread, wave-opacity;
|
||||||
|
@keyframes wave-spread {
|
||||||
|
from {
|
||||||
|
box-shadow: 0 0 0.5px 0 var(--ripple-color);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
box-shadow: 0 0 0.5px 5.5px var(--ripple-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes wave-opacity {
|
||||||
|
from {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > .btn-icon + .btn-content {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-default {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
&:hover {
|
||||||
|
color: #4096ff !important;
|
||||||
|
border-color: #4096ff;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: #0958d9 !important;
|
||||||
|
border-color: #0958d9;
|
||||||
|
}
|
||||||
|
.btn-icon {
|
||||||
|
:deep(svg) {
|
||||||
|
transition: fill 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-reverse {
|
||||||
|
.btn-default();
|
||||||
|
&:hover {
|
||||||
|
color: #fff !important;
|
||||||
|
background-color: #4096ff;
|
||||||
|
border-color: #4096ff;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: #fff !important;
|
||||||
|
background-color: #0958d9;
|
||||||
|
border-color: #0958d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: @primary;
|
||||||
|
border-color: @primary;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #4096ff;
|
||||||
|
border-color: #4096ff;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #0958d9;
|
||||||
|
border-color: #0958d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
color: #fff;
|
||||||
|
background-color: @danger;
|
||||||
|
border-color: @danger;
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #d9363e;
|
||||||
|
border-color: #d9363e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-dashed {
|
||||||
|
.btn-default();
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
.btn-text {
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-link {
|
||||||
|
color: @primary;
|
||||||
|
&:hover {
|
||||||
|
color: #4096ff;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: #0958d9;
|
||||||
|
}
|
||||||
|
.btn-icon {
|
||||||
|
:deep(svg) {
|
||||||
|
transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-small {
|
||||||
|
font-size: 14px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.btn-middle {
|
||||||
|
font-size: 14px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 4px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.btn-large {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 6.428571428571429px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.loading-small,
|
||||||
|
.loading-middle {
|
||||||
|
.btn-loading {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 1em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading-large {
|
||||||
|
.btn-loading {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 1em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-icon-only {
|
||||||
|
width: 32px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
.btn-loading,
|
||||||
|
.btn-icon {
|
||||||
|
transform: scale(1.143);
|
||||||
|
}
|
||||||
|
.btn-loading {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-small.btn-icon-only {
|
||||||
|
width: 24px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.btn-large.btn-icon-only {
|
||||||
|
width: 40px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.btn-circle {
|
||||||
|
min-width: 32px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.btn-small.btn-circle {
|
||||||
|
min-width: 24px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.btn-large.btn-circle {
|
||||||
|
min-width: 40px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.btn-round {
|
||||||
|
border-radius: 32px;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
.btn-small.btn-round {
|
||||||
|
border-radius: 24px;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
.btn-large.btn-round {
|
||||||
|
border-radius: 40px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.btn-icon-only.btn-round,
|
||||||
|
.btn-small.btn-icon-only.btn-round,
|
||||||
|
.btn-large.btn-icon-only.btn-round {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.btn-loading-blur {
|
||||||
|
opacity: 0.65;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.btn-primary.btn-ghost:not(.btn-disabled) {
|
||||||
|
color: @primary;
|
||||||
|
border-color: @primary;
|
||||||
|
background-color: transparent;
|
||||||
|
&:hover {
|
||||||
|
color: #4096ff;
|
||||||
|
border-color: #4096ff;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: #0958d9;
|
||||||
|
border-color: #0958d9;
|
||||||
|
}
|
||||||
|
.btn-icon {
|
||||||
|
:deep(svg) {
|
||||||
|
transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-danger.btn-ghost:not(.btn-disabled) {
|
||||||
|
color: @danger;
|
||||||
|
border-color: @danger;
|
||||||
|
background-color: transparent;
|
||||||
|
&:hover {
|
||||||
|
color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: #d9363e;
|
||||||
|
border-color: #d9363e;
|
||||||
|
}
|
||||||
|
.btn-icon {
|
||||||
|
:deep(svg) {
|
||||||
|
transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.btn-disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
&:not(.btn-text, .btn-link) {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
&:not(.btn-text, .btn-link):hover,
|
||||||
|
&:not(.btn-text, .btn-link):active {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
color: rgba(0, 0, 0, 0.25) !important;
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
&.btn-text:hover,
|
||||||
|
&.btn-text:active {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
&.btn-link:hover,
|
||||||
|
&.btn-link:active {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
171
src/components/MyUI/Card/Card.vue
Normal file
171
src/components/MyUI/Card/Card.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import Skeleton from '../Skeleton/Skeleton.vue';
|
||||||
|
import { useSlotsExist } from '../Utils';
|
||||||
|
interface Props {
|
||||||
|
width?: number | string // 卡片宽度,单位 px
|
||||||
|
bordered?: boolean // 是否有边框
|
||||||
|
size?: 'small' | 'middle' | 'large' // 卡片的尺寸
|
||||||
|
hoverable?: boolean // 鼠标移过时可浮起
|
||||||
|
loading?: boolean // 当卡片内容还在加载中时,可以用 loading 展示一个占位
|
||||||
|
skeletonProps?: object // 加载中时,骨架屏的属性配置,参考 Skeleton Props
|
||||||
|
title?: string // 卡片标题 string | slot
|
||||||
|
extra?: string // 卡片右上角的操作区域 string | slot
|
||||||
|
headStyle?: CSSProperties // 自定义标题区域样式
|
||||||
|
bodyStyle?: CSSProperties // 自定义内容区域样式
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 'auto',
|
||||||
|
bordered: true,
|
||||||
|
size: 'middle',
|
||||||
|
hoverable: false,
|
||||||
|
loading: false,
|
||||||
|
skeletonProps: () => ({}),
|
||||||
|
title: undefined,
|
||||||
|
extra: undefined,
|
||||||
|
headStyle: () => ({}),
|
||||||
|
bodyStyle: () => ({})
|
||||||
|
});
|
||||||
|
const slotsExist = useSlotsExist(['title', 'extra']);
|
||||||
|
const cardWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const showHeader = computed(() => {
|
||||||
|
return slotsExist.title || slotsExist.extra || props.title || props.extra;
|
||||||
|
});
|
||||||
|
const showTitle = computed(() => {
|
||||||
|
return slotsExist.title || props.title;
|
||||||
|
});
|
||||||
|
const showExtra = computed(() => {
|
||||||
|
return slotsExist.extra || props.extra;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-card"
|
||||||
|
:class="{
|
||||||
|
'card-bordered': bordered,
|
||||||
|
'card-small': size === 'small',
|
||||||
|
'card-middle': size === 'middle',
|
||||||
|
'card-large': size === 'large',
|
||||||
|
'card-hoverable': hoverable
|
||||||
|
}"
|
||||||
|
:style="`width: ${cardWidth};`"
|
||||||
|
>
|
||||||
|
<div class="m-card-head" :style="headStyle" v-if="showHeader">
|
||||||
|
<div class="m-head-wrapper">
|
||||||
|
<div v-if="showTitle" class="head-title">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="showExtra" class="head-extra">
|
||||||
|
<slot name="extra">{{ extra }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-card-body" :style="bodyStyle">
|
||||||
|
<Skeleton :title="false" :loading="loading" v-bind="skeletonProps">
|
||||||
|
<slot></slot>
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-card {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
position: relative;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
transition: width 0.2s;
|
||||||
|
.m-card-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: 600;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
.m-head-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
.head-title {
|
||||||
|
display: inline-block;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.head-extra {
|
||||||
|
margin-left: auto;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: font-size 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.m-card-body {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
transition: padding 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-bordered {
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.card-small {
|
||||||
|
.m-card-head {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.m-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-middle {
|
||||||
|
.m-card-head {
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 0 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.m-card-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-large {
|
||||||
|
font-size: 16px;
|
||||||
|
.m-card-head {
|
||||||
|
min-height: 74px;
|
||||||
|
padding: 0 36px;
|
||||||
|
font-size: 18px;
|
||||||
|
.m-head-wrapper .head-extra {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.m-card-body {
|
||||||
|
padding: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-hoverable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
|
&:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 1px 2px -2px rgba(0, 0, 0, 0.16),
|
||||||
|
0 3px 6px 0 rgba(0, 0, 0, 0.12),
|
||||||
|
0 5px 12px 4px rgba(0, 0, 0, 0.09);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
658
src/components/MyUI/Carousel/Carousel.vue
Normal file
658
src/components/MyUI/Carousel/Carousel.vue
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed, ref, watch} from 'vue';
|
||||||
|
import {cancelRaf, rafTimeout, useEventListener, useResizeObserver} from '../Utils';
|
||||||
|
import {useTransition} from '@vueuse/core';
|
||||||
|
import Spin from '../Spin/Spin.vue';
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
title?: string // 图片名称
|
||||||
|
src: string // 图片地址
|
||||||
|
link?: string // 图片跳转链接
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
images?: Image[] // 走马灯图片数组
|
||||||
|
width?: number | string // 走马灯宽度,单位 px
|
||||||
|
height?: number | string // 走马灯高度,单位 px
|
||||||
|
autoplay?: boolean // 是否自动轮播
|
||||||
|
pauseOnMouseEnter?: boolean // 当鼠标移入走马灯时,是否暂停自动轮播
|
||||||
|
effect?: 'slide' | 'fade' // 轮播图切换时的过渡效果
|
||||||
|
interval?: number // 自动轮播间隔,单位 ms
|
||||||
|
showArrow?: boolean // 是否显示箭头
|
||||||
|
arrowColor?: string // 箭头颜色
|
||||||
|
arrowSize?: number // 箭头大小,单位 px
|
||||||
|
dots?: boolean // 是否显示指示点
|
||||||
|
dotSize?: number // 指示点大小,单位 px
|
||||||
|
dotColor?: string // 指示点颜色
|
||||||
|
dotActiveColor?: string // 指示点选中颜色
|
||||||
|
dotStyle?: CSSProperties // 指示点样式,优先级高于 dotSize、dotColor
|
||||||
|
dotActiveStyle?: CSSProperties // 指示点选中样式,优先级高于 dotActiveColor
|
||||||
|
dotPosition?: 'bottom' | 'top' | 'left' | 'right' // 指示点位置,位置为 'left' | 'right' 时,effect: 'slide' 轮播自动变为垂直轮播
|
||||||
|
dotsTrigger?: 'click' | 'hover' // 指示点触发切换的方式
|
||||||
|
spinProps?: object // 图片加载中样式,Spin 组件属性配置,参考 Spin Props
|
||||||
|
fadeDuration?: number // 渐变动画持续时长,单位 ms,仅当 effect 为 'fade' 时生效
|
||||||
|
fadeFunction?: string // 渐变动画函数,仅当 effect 为 'fade' 时生效,可参考 transition-timing-function 写法:https://developer.mozilla.org/zh-CN/docs/Web/CSS/transition-timing-function
|
||||||
|
slideDuration?: number // 滑动动画持续时长,单位 ms,仅当 effect 为 'slide' 时生效
|
||||||
|
slideFunction?: string | number[] // 滑动动画函数,仅当 effect 为 'slide' 时生效,可参考 useTransition 写法:https://vueuse.org/core/useTransition/#usage
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
images: () => [],
|
||||||
|
width: '100%',
|
||||||
|
height: '100vh',
|
||||||
|
autoplay: false,
|
||||||
|
pauseOnMouseEnter: false,
|
||||||
|
effect: 'slide',
|
||||||
|
interval: 3000,
|
||||||
|
showArrow: true,
|
||||||
|
arrowColor: '#FFF',
|
||||||
|
arrowSize: 36,
|
||||||
|
dots: true,
|
||||||
|
dotSize: 10,
|
||||||
|
dotColor: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
dotActiveColor: '#1677FF',
|
||||||
|
dotStyle: () => ({}),
|
||||||
|
dotActiveStyle: () => ({}),
|
||||||
|
dotPosition: 'bottom',
|
||||||
|
dotsTrigger: 'click',
|
||||||
|
spinProps: () => ({}),
|
||||||
|
fadeDuration: 500,
|
||||||
|
fadeFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
slideDuration: 800,
|
||||||
|
slideFunction: () => [0.65, 0, 0.35, 1]
|
||||||
|
});
|
||||||
|
const offset = ref(0); // 滑动偏移值
|
||||||
|
const slideTimer = ref(); // 轮播切换定时器
|
||||||
|
const stopCarousel = ref(false); // 鼠标悬浮时,停止切换标志
|
||||||
|
const switchPrevent = ref(false); // 在滑动切换过程中,禁用其他所有切换操作
|
||||||
|
const moveEffectRaf = ref(); // 移动过程 requestAnimationFrame 的返回值,一个 long 整数,请求 ID,是回调列表中唯一的标识
|
||||||
|
const targetPosition = ref(); // 目标移动位置
|
||||||
|
const carouselRef = ref(); // carousel DOM 引用
|
||||||
|
const activeSwitcher = ref(1); // 当前展示图片标识
|
||||||
|
const imageWidth = ref(); // 图片宽度
|
||||||
|
const imageHeight = ref(); // 图片高度
|
||||||
|
const complete = ref(Array(props.images.length).fill(false)); // 图片是否加载完成
|
||||||
|
const emits = defineEmits(['change', 'click']);
|
||||||
|
const carouselWidth = computed(() => {
|
||||||
|
// 走马灯区域宽度
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const carouselHeight = computed(() => {
|
||||||
|
// 走马灯区域高度
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
} else {
|
||||||
|
return props.height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const imageAmount = computed(() => {
|
||||||
|
// 轮播图片数量
|
||||||
|
return props.images.length;
|
||||||
|
});
|
||||||
|
const verticalSlide = computed(() => {
|
||||||
|
// 是否垂直轮播
|
||||||
|
return ['left', 'right'].includes(props.dotPosition);
|
||||||
|
});
|
||||||
|
const moveUnitDistance = computed(() => {
|
||||||
|
// 每次移动的单位距离
|
||||||
|
if (verticalSlide.value) {
|
||||||
|
return imageHeight.value;
|
||||||
|
} else {
|
||||||
|
return imageWidth.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const carouselStyle = computed(() => {
|
||||||
|
if (props.effect === 'slide') {
|
||||||
|
return {
|
||||||
|
transform: (verticalSlide.value ? 'translateY' : 'translateX') + `(${-offset.value}px)`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
verticalSlide.value,
|
||||||
|
props.effect,
|
||||||
|
props.images,
|
||||||
|
props.autoplay,
|
||||||
|
props.interval,
|
||||||
|
props.fadeDuration,
|
||||||
|
props.fadeFunction,
|
||||||
|
complete.value[0]
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
initCarousel();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
flush: 'post'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(activeSwitcher, (to) => {
|
||||||
|
emits('change', to);
|
||||||
|
});
|
||||||
|
useEventListener(document, 'visibilitychange', visibilityChange);
|
||||||
|
useResizeObserver(carouselRef, () => {
|
||||||
|
getImageSize();
|
||||||
|
initCarousel();
|
||||||
|
});
|
||||||
|
|
||||||
|
function initCarousel() {
|
||||||
|
if (slideTimer.value) {
|
||||||
|
cancelRaf(slideTimer.value);
|
||||||
|
}
|
||||||
|
if (moveEffectRaf.value) {
|
||||||
|
cancelAnimationFrame(moveEffectRaf.value);
|
||||||
|
}
|
||||||
|
switchPrevent.value = false;
|
||||||
|
if (props.effect === 'slide') {
|
||||||
|
offset.value = (activeSwitcher.value - 1) * moveUnitDistance.value;
|
||||||
|
}
|
||||||
|
onStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onComplete(index: number) {
|
||||||
|
// 图片加载完成
|
||||||
|
complete.value[index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageSize() {
|
||||||
|
// 获取每张图片大小
|
||||||
|
imageWidth.value = carouselRef.value.offsetWidth;
|
||||||
|
imageHeight.value = carouselRef.value.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyboard(e: KeyboardEvent) {
|
||||||
|
if (imageAmount.value > 1) {
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
onLeftArrow();
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
|
onRightArrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当用户导航到新页面、切换标签页、关闭标签页、最小化或关闭浏览器,或者在移动设备上从浏览器切换到不同的应用程序时,暂停切换
|
||||||
|
function visibilityChange() {
|
||||||
|
console.log('visibilityState', document.visibilityState);
|
||||||
|
const visibility = document.visibilityState;
|
||||||
|
if (visibility === 'hidden') {
|
||||||
|
if (slideTimer.value) {
|
||||||
|
cancelRaf(slideTimer.value);
|
||||||
|
}
|
||||||
|
// hidden
|
||||||
|
offset.value = originNumber.value + distance.value;
|
||||||
|
switchPrevent.value = false;
|
||||||
|
} else {
|
||||||
|
// visible
|
||||||
|
onStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStart() {
|
||||||
|
if (props.autoplay && imageAmount.value > 1 && complete.value[0]) {
|
||||||
|
// 超过一条时滑动
|
||||||
|
stopCarousel.value = false;
|
||||||
|
autoSlide(); // 自动滑动轮播
|
||||||
|
console.log('Carousel Start');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStop() {
|
||||||
|
if (slideTimer.value) {
|
||||||
|
cancelRaf(slideTimer.value);
|
||||||
|
}
|
||||||
|
stopCarousel.value = true;
|
||||||
|
console.log('Carousel Stop');
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoSlide() {
|
||||||
|
if (!stopCarousel.value) {
|
||||||
|
if (slideTimer.value) {
|
||||||
|
cancelRaf(slideTimer.value);
|
||||||
|
}
|
||||||
|
slideTimer.value = rafTimeout(() => {
|
||||||
|
switchPrevent.value = true; // 禁用导航切换
|
||||||
|
if (props.effect === 'slide') {
|
||||||
|
const target = (offset.value % (imageAmount.value * moveUnitDistance.value)) + moveUnitDistance.value;
|
||||||
|
moveLeft(target);
|
||||||
|
activeSwitcher.value = (activeSwitcher.value % imageAmount.value) + 1;
|
||||||
|
} else {
|
||||||
|
// fade
|
||||||
|
moveFade('left');
|
||||||
|
}
|
||||||
|
}, props.interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeftArrow() {
|
||||||
|
if (!switchPrevent.value) {
|
||||||
|
switchPrevent.value = true;
|
||||||
|
if (slideTimer.value) {
|
||||||
|
cancelRaf(slideTimer.value);
|
||||||
|
}
|
||||||
|
if (props.effect === 'slide') {
|
||||||
|
const target = ((activeSwitcher.value + imageAmount.value - 2) % imageAmount.value) * moveUnitDistance.value;
|
||||||
|
moveRight(target);
|
||||||
|
activeSwitcher.value = activeSwitcher.value - 1 > 0 ? activeSwitcher.value - 1 : imageAmount.value;
|
||||||
|
} else {
|
||||||
|
// fade
|
||||||
|
moveFade('right');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRightArrow() {
|
||||||
|
if (!switchPrevent.value) {
|
||||||
|
switchPrevent.value = true;
|
||||||
|
if (slideTimer.value) {
|
||||||
|
cancelRaf(slideTimer.value);
|
||||||
|
}
|
||||||
|
if (props.effect === 'slide') {
|
||||||
|
const target = activeSwitcher.value * moveUnitDistance.value;
|
||||||
|
moveLeft(target);
|
||||||
|
activeSwitcher.value = (activeSwitcher.value % imageAmount.value) + 1;
|
||||||
|
} else {
|
||||||
|
// fade
|
||||||
|
moveFade('left');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseNumber = ref(0);
|
||||||
|
const originNumber = ref(0); // 初始位置
|
||||||
|
const distance = ref(0); // 滑动距离
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
const cubicBezierNumber = useTransition(baseNumber, {
|
||||||
|
duration: props.slideDuration, // 过渡动画时长
|
||||||
|
transition: props.slideFunction // 过渡动画函数
|
||||||
|
});
|
||||||
|
|
||||||
|
function moveFade(direction: 'left' | 'right' | 'switch', n?: number) {
|
||||||
|
if (direction === 'left') {
|
||||||
|
activeSwitcher.value = (activeSwitcher.value % imageAmount.value) + 1;
|
||||||
|
} else if (direction === 'right') {
|
||||||
|
activeSwitcher.value = activeSwitcher.value - 1 > 0 ? activeSwitcher.value - 1 : imageAmount.value;
|
||||||
|
} else {
|
||||||
|
activeSwitcher.value = n as number;
|
||||||
|
}
|
||||||
|
rafTimeout(() => {
|
||||||
|
switchPrevent.value = false;
|
||||||
|
if (props.autoplay) {
|
||||||
|
autoSlide();
|
||||||
|
}
|
||||||
|
}, props.fadeDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNumber(target: number) {
|
||||||
|
targetPosition.value = target;
|
||||||
|
baseNumber.value = baseNumber.value ? 0 : 1;
|
||||||
|
originNumber.value = offset.value; // 初始位置
|
||||||
|
distance.value = target - originNumber.value; // 总距离
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveEffect() {
|
||||||
|
// 滑动效果函数
|
||||||
|
if (baseNumber.value) {
|
||||||
|
offset.value = originNumber.value + distance.value * cubicBezierNumber.value;
|
||||||
|
} else {
|
||||||
|
offset.value = originNumber.value + distance.value * (1 - cubicBezierNumber.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveLeftEffect() {
|
||||||
|
if (offset.value >= targetPosition.value) {
|
||||||
|
switchPrevent.value = false;
|
||||||
|
if (props.autoplay) {
|
||||||
|
autoSlide(); // 自动间隔切换下一张
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
moveEffect();
|
||||||
|
moveEffectRaf.value = requestAnimationFrame(moveLeftEffect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveLeft(target: number) {
|
||||||
|
// 箭头切换或跳转切换,向左滑动效果
|
||||||
|
if (offset.value === imageAmount.value * moveUnitDistance.value) {
|
||||||
|
// 最后一张时,重置left
|
||||||
|
offset.value = 0;
|
||||||
|
}
|
||||||
|
toggleNumber(target);
|
||||||
|
moveEffectRaf.value = requestAnimationFrame(moveLeftEffect);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveRightEffect() {
|
||||||
|
if (offset.value <= targetPosition.value) {
|
||||||
|
switchPrevent.value = false;
|
||||||
|
if (props.autoplay) {
|
||||||
|
autoSlide();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
moveEffect();
|
||||||
|
moveEffectRaf.value = requestAnimationFrame(moveRightEffect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveRight(target: number) {
|
||||||
|
// 箭头切换或跳转切换,向右滑动效果
|
||||||
|
if (offset.value === 0) {
|
||||||
|
// 第一张时,重置left
|
||||||
|
offset.value = imageAmount.value * moveUnitDistance.value;
|
||||||
|
}
|
||||||
|
toggleNumber(target);
|
||||||
|
moveEffectRaf.value = requestAnimationFrame(moveRightEffect);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSwitch(n: number) {
|
||||||
|
// 分页切换图片
|
||||||
|
if (!switchPrevent.value && activeSwitcher.value !== n) {
|
||||||
|
switchPrevent.value = true;
|
||||||
|
if (slideTimer.value) {
|
||||||
|
cancelRaf(slideTimer.value);
|
||||||
|
}
|
||||||
|
if (n < activeSwitcher.value) {
|
||||||
|
// 往右滑动
|
||||||
|
if (props.effect === 'slide') {
|
||||||
|
const target = (n - 1) * moveUnitDistance.value;
|
||||||
|
moveRight(target);
|
||||||
|
activeSwitcher.value = n;
|
||||||
|
} else {
|
||||||
|
// fade
|
||||||
|
moveFade('switch', n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (n > activeSwitcher.value) {
|
||||||
|
// 往左滑动
|
||||||
|
if (props.effect === 'slide') {
|
||||||
|
const target = (n - 1) * moveUnitDistance.value;
|
||||||
|
moveLeft(target);
|
||||||
|
activeSwitcher.value = n;
|
||||||
|
} else {
|
||||||
|
// fade
|
||||||
|
moveFade('switch', n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseEnter(n: number) {
|
||||||
|
onSwitch(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickImage(image: Image) {
|
||||||
|
emits('click', image);
|
||||||
|
}
|
||||||
|
|
||||||
|
function to(n: number): void {
|
||||||
|
if (n >= 1 && n <= imageAmount.value) {
|
||||||
|
onSwitch(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev(): void {
|
||||||
|
onLeftArrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
function next(): void {
|
||||||
|
onRightArrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentIndex(): number {
|
||||||
|
return activeSwitcher.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
to,
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
getCurrentIndex
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="carouselRef"
|
||||||
|
class="m-carousel"
|
||||||
|
:class="{ 'carousel-vertical': verticalSlide, 'carousel-fade': effect === 'fade' }"
|
||||||
|
:style="`--arrow-color: ${arrowColor}; --dot-size: ${dotSize}px; --dot-color: ${dotColor}; --fade-duration: ${props.fadeDuration}ms; --fade-function: ${props.fadeFunction}; width: ${carouselWidth}; height: ${carouselHeight};`"
|
||||||
|
@mouseenter="autoplay && pauseOnMouseEnter ? onStop() : () => false"
|
||||||
|
@mouseleave="autoplay && pauseOnMouseEnter ? onStart() : () => false"
|
||||||
|
>
|
||||||
|
<div class="m-carousel-flex" :style="carouselStyle">
|
||||||
|
<div
|
||||||
|
class="m-image"
|
||||||
|
:class="{ 'image-fade-active': effect === 'fade' && activeSwitcher === index + 1 }"
|
||||||
|
@click="clickImage(image)"
|
||||||
|
v-for="(image, index) in images"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<Spin :spinning="!complete[index]" indicator="dynamic-circle" v-bind="spinProps">
|
||||||
|
<img
|
||||||
|
@load="onComplete(index)"
|
||||||
|
:src="image.src"
|
||||||
|
:key="image.src"
|
||||||
|
:alt="image.title"
|
||||||
|
class="u-image"
|
||||||
|
:style="`width: ${imageWidth}px; height: ${imageHeight}px;`"
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
<div class="m-image" @click="clickImage(images[0])" v-if="imageAmount && effect === 'slide'">
|
||||||
|
<Spin :spinning="!complete[0]" indicator="dynamic-circle" v-bind="spinProps">
|
||||||
|
<img
|
||||||
|
@load="onComplete(0)"
|
||||||
|
:src="images[0].src"
|
||||||
|
:key="images[0].src"
|
||||||
|
:alt="images[0].title"
|
||||||
|
class="u-image"
|
||||||
|
:style="`width: ${imageWidth}px; height: ${imageHeight}px;`"
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="showArrow">
|
||||||
|
<svg
|
||||||
|
tabindex="0"
|
||||||
|
class="arrow-left"
|
||||||
|
:style="`width: ${arrowSize}px; height: ${arrowSize}px;`"
|
||||||
|
@click="onLeftArrow"
|
||||||
|
@keydown.prevent="onKeyboard"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.26 3.2a.75.75 0 0 1 .04 1.06L6.773 8l3.527 3.74a.75.75 0 1 1-1.1 1.02l-4-4.25a.75.75 0 0 1 0-1.02l4-4.25a.75.75 0 0 1 1.06-.04z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
tabindex="0"
|
||||||
|
class="arrow-right"
|
||||||
|
:style="`width: ${arrowSize}px; height: ${arrowSize}px;`"
|
||||||
|
@click="onRightArrow"
|
||||||
|
@keydown.prevent="onKeyboard"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.74 3.2a.75.75 0 0 0-.04 1.06L9.227 8L5.7 11.74a.75.75 0 1 0 1.1 1.02l4-4.25a.75.75 0 0 0 0-1.02l-4-4.25a.75.75 0 0 0-1.06-.04z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<div class="m-switch" :class="`switch-${dotPosition}`" v-if="dots">
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="u-dot"
|
||||||
|
:style="[dotStyle, activeSwitcher === n ? { backgroundColor: dotActiveColor, ...dotActiveStyle } : {}]"
|
||||||
|
v-for="n in imageAmount"
|
||||||
|
:key="n"
|
||||||
|
@click="dotsTrigger === 'click' ? onSwitch(n) : () => false"
|
||||||
|
@mouseenter="dotsTrigger === 'hover' ? onMouseEnter(n) : () => false"
|
||||||
|
@keydown.prevent="onKeyboard"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-carousel {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.m-carousel-flex {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
// will-change: transform;
|
||||||
|
.m-image {
|
||||||
|
// 指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值
|
||||||
|
flex-shrink: 0; // 默认为 1,为 0 时不缩小
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.u-image {
|
||||||
|
display: inline-block;
|
||||||
|
object-fit: cover;
|
||||||
|
vertical-align: bottom; // 消除img标签底部的5px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.arrow-left {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-right {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-left {
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--arrow-color);
|
||||||
|
fill: currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
outline: none;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-right {
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--arrow-color);
|
||||||
|
fill: currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
outline: none;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-switch {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9;
|
||||||
|
bottom: 12px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
.u-dot {
|
||||||
|
// flex: 0 1 auto;
|
||||||
|
width: var(--dot-size);
|
||||||
|
height: var(--dot-size);
|
||||||
|
border-radius: var(--dot-size);
|
||||||
|
background-color: var(--dot-color);
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-top {
|
||||||
|
top: 12px;
|
||||||
|
bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-left {
|
||||||
|
left: 12px;
|
||||||
|
right: auto;
|
||||||
|
top: 50%;
|
||||||
|
bottom: auto;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-right {
|
||||||
|
right: 12px;
|
||||||
|
left: auto;
|
||||||
|
top: 50%;
|
||||||
|
bottom: auto;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-vertical {
|
||||||
|
.m-carousel-flex {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-left {
|
||||||
|
top: 6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-right {
|
||||||
|
top: auto;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-fade {
|
||||||
|
.m-image {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: var(--fade-duration);
|
||||||
|
transition-timing-function: var(--fade-function);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-fade-active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
188
src/components/MyUI/Cascader/Cascader.vue
Normal file
188
src/components/MyUI/Cascader/Cascader.vue
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, watchEffect} from 'vue';
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label?: string // 选项名
|
||||||
|
value?: string | number // 选项值
|
||||||
|
disabled?: boolean // 是否禁用选项,默认 false
|
||||||
|
children?: Option[] // 选项 children 数组
|
||||||
|
[propName: string]: any // 添加一个字符串索引签名,用于包含带有任意数量的其他属性
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options?: Option[] // 可选项数据源
|
||||||
|
label?: string // 下拉字典项的文本字段名
|
||||||
|
value?: string // 下拉字典项的值字段名
|
||||||
|
children?: string // 下拉字典项的后代字段名
|
||||||
|
placeholder?: string | string[] // 三级下拉各自占位文本
|
||||||
|
changeOnSelect?: boolean // 当此项为 true 时,点选每级菜单选项值(v-model)都会发生变化;否则只有选择第三级选项后选项值才会变化
|
||||||
|
gap?: number // 级联下拉框相互间隙宽度,单位 px
|
||||||
|
width?: 'auto' | number | number[] // 三级下拉各自宽度
|
||||||
|
height?: number // 下拉框高度
|
||||||
|
disabled?: boolean | boolean[] // 三级各自是否禁用
|
||||||
|
allowClear?: boolean // 是否支持清除
|
||||||
|
search?: boolean // 是否支持搜索,使用搜索时请设置 width
|
||||||
|
/*
|
||||||
|
根据输入项进行筛选,默认为 true 时,筛选每个选项的文本字段 label 是否包含输入项,包含返回 true,反之返回 false
|
||||||
|
当其为函数 Function 时,接受 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false
|
||||||
|
*/
|
||||||
|
filter?: any | true // 过滤条件函数,仅当支持搜索时生效
|
||||||
|
maxDisplay?: number // 下拉面板最多能展示的下拉项数,超过后滚动显示
|
||||||
|
modelValue?: number[] | string[] //(v-model)级联选中项
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
options: () => [],
|
||||||
|
label: 'label',
|
||||||
|
value: 'value',
|
||||||
|
children: 'children',
|
||||||
|
placeholder: '请选择',
|
||||||
|
changeOnSelect: false,
|
||||||
|
gap: 8,
|
||||||
|
width: 'auto',
|
||||||
|
height: 32,
|
||||||
|
disabled: false,
|
||||||
|
allowClear: false,
|
||||||
|
search: false,
|
||||||
|
filter: true,
|
||||||
|
maxDisplay: 6,
|
||||||
|
modelValue: () => []
|
||||||
|
});
|
||||||
|
const values = ref<(string | number)[]>([]); // 级联value值数组
|
||||||
|
const labels = ref<string[]>([]); // 级联label文本数组
|
||||||
|
const firstOptions = ref<Option[]>([]);
|
||||||
|
const secondOptions = ref<Option[]>([]);
|
||||||
|
const thirdOptions = ref<Option[]>([]);
|
||||||
|
const emits = defineEmits(['update:modelValue', 'change']);
|
||||||
|
watchEffect(() => {
|
||||||
|
firstOptions.value = [...props.options];
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
values.value = [...props.modelValue];
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
initCascader(values.value);
|
||||||
|
initLabels(values.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function findChildren(options: Option[], index: number): Option[] {
|
||||||
|
const len = options.length;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
if (options[i][props.value] === values.value[index]) {
|
||||||
|
return options[i][props.children] || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCascader(values: (string | number)[]) {
|
||||||
|
// 获取二级/三级下拉项
|
||||||
|
secondOptions.value = findChildren(firstOptions.value, 0);
|
||||||
|
thirdOptions.value = [];
|
||||||
|
if (values.length > 1) {
|
||||||
|
thirdOptions.value = findChildren(secondOptions.value, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLabel(options: Option[], index: number): any {
|
||||||
|
const len = options.length;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
if (options[i][props.value] === values.value[index]) {
|
||||||
|
return options[i][props.label];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values.value[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLabels(values: (string | number)[]) {
|
||||||
|
labels.value[0] = findLabel(firstOptions.value, 0);
|
||||||
|
if (values.length > 1) {
|
||||||
|
labels.value[1] = findLabel(secondOptions.value, 1);
|
||||||
|
}
|
||||||
|
if (values.length > 2) {
|
||||||
|
labels.value[2] = findLabel(thirdOptions.value, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFirstChange(value: string | number, label: string) {
|
||||||
|
// 一级下拉回调
|
||||||
|
if (props.changeOnSelect) {
|
||||||
|
emits('update:modelValue', [value]);
|
||||||
|
emits('change', [value], [label]);
|
||||||
|
} else {
|
||||||
|
values.value = [value];
|
||||||
|
labels.value = [label];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSecondChange(value: string | number, label: string) {
|
||||||
|
// 二级下拉回调
|
||||||
|
if (props.changeOnSelect) {
|
||||||
|
emits('update:modelValue', [values.value[0], value]);
|
||||||
|
emits('change', [values.value[0], value], [labels.value[0], label]);
|
||||||
|
} else {
|
||||||
|
values.value = [values.value[0], value];
|
||||||
|
labels.value = [labels.value[0], label];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThirdChange(value: string | number, label: string) {
|
||||||
|
// 三级下拉回调
|
||||||
|
emits('update:modelValue', [...values.value.slice(0, 2), value]);
|
||||||
|
emits('change', [...values.value.slice(0, 2), value], [...labels.value.slice(0, 2), label]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-cascader" :style="`height: ${height}px; gap: ${gap}px;`">
|
||||||
|
<Select
|
||||||
|
:options="firstOptions"
|
||||||
|
:label="label"
|
||||||
|
:value="value"
|
||||||
|
:placeholder="Array.isArray(placeholder) ? placeholder[0] : placeholder"
|
||||||
|
:disabled="Array.isArray(disabled) ? disabled[0] : disabled"
|
||||||
|
:allow-clear="allowClear"
|
||||||
|
:search="search"
|
||||||
|
:filter="filter"
|
||||||
|
:width="Array.isArray(width) ? width[0] : width"
|
||||||
|
:height="height"
|
||||||
|
:max-display="maxDisplay"
|
||||||
|
v-model="values[0]"
|
||||||
|
@change="onFirstChange"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
:options="secondOptions"
|
||||||
|
:label="label"
|
||||||
|
:value="value"
|
||||||
|
:placeholder="Array.isArray(placeholder) ? placeholder[1] : placeholder"
|
||||||
|
:disabled="Array.isArray(disabled) ? disabled[1] : disabled"
|
||||||
|
:allow-clear="allowClear"
|
||||||
|
:search="search"
|
||||||
|
:filter="filter"
|
||||||
|
:width="Array.isArray(width) ? width[1] : width"
|
||||||
|
:height="height"
|
||||||
|
:max-display="maxDisplay"
|
||||||
|
v-model="values[1]"
|
||||||
|
@change="onSecondChange"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
:options="thirdOptions"
|
||||||
|
:label="label"
|
||||||
|
:value="value"
|
||||||
|
:placeholder="Array.isArray(placeholder) ? placeholder[2] : placeholder"
|
||||||
|
:disabled="Array.isArray(disabled) ? disabled[2] : disabled"
|
||||||
|
:allow-clear="allowClear"
|
||||||
|
:search="search"
|
||||||
|
:filter="filter"
|
||||||
|
:width="Array.isArray(width) ? width[2] : width"
|
||||||
|
:height="height"
|
||||||
|
:max-display="maxDisplay"
|
||||||
|
v-model="values[2]"
|
||||||
|
@change="onThirdChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-cascader {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
249
src/components/MyUI/Checkbox/Checkbox.vue
Normal file
249
src/components/MyUI/Checkbox/Checkbox.vue
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, watchEffect} from 'vue';
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string // 选项名
|
||||||
|
value: string | number // 选项值
|
||||||
|
disabled?: boolean // 是否禁用选项
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options?: Option[] // 复选框选项数据
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
vertical?: boolean // 是否垂直排列
|
||||||
|
value?: (string | number)[] // (v-model) 当前选中的值,配合 options 使用
|
||||||
|
gap?: number | number[] // 多个复选框之间的间距;垂直排列时为垂直间距,单位 px;数组间距用于水平排列折行时:[水平间距, 垂直间距]
|
||||||
|
width?: string | number // 复选区域最大宽度,超出后折行,单位 px
|
||||||
|
height?: string | number // 复选区域最大高度,超出后滚动,单位 px
|
||||||
|
indeterminate?: boolean // 全选时的样式控制
|
||||||
|
checked?: boolean // (v-model) 当前是否选中
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
options: () => [],
|
||||||
|
disabled: false,
|
||||||
|
vertical: false,
|
||||||
|
value: () => [],
|
||||||
|
gap: 8,
|
||||||
|
width: 'auto',
|
||||||
|
height: 'auto',
|
||||||
|
indeterminate: false,
|
||||||
|
checked: false
|
||||||
|
});
|
||||||
|
const checkboxChecked = ref<boolean>();
|
||||||
|
const optionsCheckedValue = ref<any[]>([]);
|
||||||
|
const emits = defineEmits(['update:value', 'update:checked', 'change']);
|
||||||
|
const optionsAmount = computed(() => {
|
||||||
|
// 选项总数
|
||||||
|
return props.options.length;
|
||||||
|
});
|
||||||
|
const maxWidth = computed(() => {
|
||||||
|
// 复选区域最大宽度
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const maxHeight = computed(() => {
|
||||||
|
// 复选区域最大高度
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
} else {
|
||||||
|
return props.height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const gapValue = computed(() => {
|
||||||
|
if (!props.vertical && Array.isArray(props.gap)) {
|
||||||
|
return `${props.gap[1]}px ${props.gap[0]}px`;
|
||||||
|
}
|
||||||
|
return `${props.gap}px`;
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
checkboxChecked.value = props.checked;
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
optionsCheckedValue.value = props.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkDisabled(disabled: boolean | undefined) {
|
||||||
|
if (disabled === undefined) {
|
||||||
|
return props.disabled;
|
||||||
|
} else {
|
||||||
|
return disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(value: string | number) {
|
||||||
|
if (optionsCheckedValue.value.includes(value)) {
|
||||||
|
// 已选中
|
||||||
|
const newVal = optionsCheckedValue.value.filter((target) => target !== value);
|
||||||
|
optionsCheckedValue.value = newVal;
|
||||||
|
emits('update:value', newVal);
|
||||||
|
emits('change', newVal);
|
||||||
|
} else {
|
||||||
|
// 未选中
|
||||||
|
const newVal = [...optionsCheckedValue.value, value];
|
||||||
|
optionsCheckedValue.value = newVal;
|
||||||
|
emits('update:value', newVal);
|
||||||
|
emits('change', newVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChecked() {
|
||||||
|
checkboxChecked.value = !checkboxChecked.value;
|
||||||
|
emits('update:checked', checkboxChecked.value);
|
||||||
|
emits('change', checkboxChecked.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-checkbox"
|
||||||
|
:class="{ 'checkbox-vertical': vertical }"
|
||||||
|
:style="`--checkbox-gap: ${gapValue}; --checkbox-max-width: ${maxWidth}; --checkbox-max-height: ${maxHeight};`"
|
||||||
|
>
|
||||||
|
<template v-if="optionsAmount">
|
||||||
|
<div class="m-checkbox-wrap" v-for="(option, index) in options" :key="index">
|
||||||
|
<div
|
||||||
|
class="m-checkbox-box"
|
||||||
|
:class="{ 'checkbox-disabled': checkDisabled(option.disabled) }"
|
||||||
|
@click="checkDisabled(option.disabled) ? () => false : onClick(option.value)"
|
||||||
|
>
|
||||||
|
<span class="checkbox-box" :class="{ 'checkbox-checked': optionsCheckedValue.includes(option.value) }"></span>
|
||||||
|
<span class="checkbox-label">
|
||||||
|
<slot :label="option.label">{{ option.label }}</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="m-checkbox-wrap">
|
||||||
|
<div
|
||||||
|
class="m-checkbox-box"
|
||||||
|
:class="{ 'checkbox-disabled': disabled }"
|
||||||
|
@click="disabled ? () => false : onChecked()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="checkbox-box"
|
||||||
|
:class="{
|
||||||
|
'checkbox-checked': checkboxChecked && !indeterminate,
|
||||||
|
'checkbox-indeterminate': indeterminate
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<span class="checkbox-label">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-checkbox {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--checkbox-gap);
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
max-width: var(--checkbox-max-width);
|
||||||
|
max-height: var(--checkbox-max-height);
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.m-checkbox-wrap {
|
||||||
|
.m-checkbox-box {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:not(.checkbox-disabled):hover {
|
||||||
|
.checkbox-box {
|
||||||
|
border-color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-box {
|
||||||
|
/*
|
||||||
|
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小
|
||||||
|
如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
|
||||||
|
*/
|
||||||
|
flex-shrink: 0; // 默认 1.即空间不足时,项目将缩小
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 21.5%;
|
||||||
|
display: table;
|
||||||
|
width: 5.7142857142857135px;
|
||||||
|
height: 9.142857142857142px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 0;
|
||||||
|
transform: rotate(45deg) scale(0) translate(-50%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
content: '';
|
||||||
|
transition: all 0.1s cubic-bezier(0.71, -0.46, 0.88, 0.6),
|
||||||
|
opacity 0.1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-checked {
|
||||||
|
background-color: @themeColor;
|
||||||
|
border-color: @themeColor;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(45deg) scale(1) translate(-50%, -50%);
|
||||||
|
transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-indeterminate {
|
||||||
|
&::after {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: @themeColor;
|
||||||
|
border: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 0 8px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.checkbox-box {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-color: rgba(0, 0, 0, 0.25);
|
||||||
|
animation-name: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
394
src/components/MyUI/Collapse/Collapse.vue
Normal file
394
src/components/MyUI/Collapse/Collapse.vue
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties, Slot} from 'vue';
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import Button from '../Button/Button.vue';
|
||||||
|
import {rafTimeout} from '../Utils';
|
||||||
|
|
||||||
|
interface Collapse {
|
||||||
|
key?: string | number // 对应 activeKey,如果没有传入 key 属性,则默认使用数据索引 (0,1,2...) 绑定
|
||||||
|
header?: string // 面板标题 string | slot
|
||||||
|
content?: string // 面板内容 string | slot
|
||||||
|
disabled?: boolean // 是否禁用展开,默认 false
|
||||||
|
showArrow?: boolean // 是否展示箭头,默认 true
|
||||||
|
extra?: string // 面板标题右侧的额外内容 string | slot
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
collapseData?: Collapse[] // 折叠面板数据,可使用 slot 替换指定 key 的 header、content、arrow、extra、lang
|
||||||
|
activeKey?: string[] | string | number[] | number | null // (v-model) 当前激活 tab 面板的 key,传入 string | number 类型时,即为手风琴模式
|
||||||
|
disabled?: boolean // 是否禁用,优先级低于 Collapse 的 disabled
|
||||||
|
collapseStyle?: CSSProperties // 设置面板的样式
|
||||||
|
bordered?: boolean // 带边框风格的折叠面板
|
||||||
|
copyable?: boolean // 是否可复制面板内容
|
||||||
|
copyProps?: object // 复制按钮属性配置,参考 Button Props
|
||||||
|
lang?: string // 面板右上角固定内容,例如标识 language string | slot
|
||||||
|
itemStyle?: CSSProperties // 设置面板容器的样式
|
||||||
|
headerStyle?: CSSProperties // 设置面板标题的样式
|
||||||
|
contentStyle?: CSSProperties // 设置面板内容的样式
|
||||||
|
arrow?: Slot // 自定义箭头切换图标 slot
|
||||||
|
showArrow?: boolean // 是否展示箭头,优先级低于 Collapse 的 showArrow
|
||||||
|
arrowPlacement?: 'left' | 'right' // 箭头位置
|
||||||
|
arrowStyle?: CSSProperties // 设置面板箭头的样式
|
||||||
|
extra?: string // 面板标题右侧的额外内容 string | slot
|
||||||
|
ghost?: boolean // 使折叠面板透明且无边框
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
collapseData: () => [],
|
||||||
|
activeKey: null,
|
||||||
|
disabled: false,
|
||||||
|
collapseStyle: () => ({}),
|
||||||
|
bordered: true,
|
||||||
|
copyable: false,
|
||||||
|
copyProps: () => ({}),
|
||||||
|
lang: undefined,
|
||||||
|
itemStyle: () => ({}),
|
||||||
|
headerStyle: () => ({}),
|
||||||
|
contentStyle: () => ({}),
|
||||||
|
arrow: undefined,
|
||||||
|
showArrow: true,
|
||||||
|
arrowPlacement: 'left',
|
||||||
|
arrowStyle: () => ({}),
|
||||||
|
extra: undefined,
|
||||||
|
ghost: false
|
||||||
|
});
|
||||||
|
const contentRef = ref(); // 面板内容的模板引用
|
||||||
|
const copyTxt = ref('Copy');
|
||||||
|
const emits = defineEmits(['update:activeKey', 'change']);
|
||||||
|
|
||||||
|
function setProperties(el: Element) {
|
||||||
|
;(el as HTMLElement).style.height =
|
||||||
|
(el.lastElementChild as HTMLElement).offsetHeight + (props.bordered && !props.ghost ? 1 : 0) + 'px'
|
||||||
|
;(el as HTMLElement).style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeProperties(el: Element) {
|
||||||
|
;(el as HTMLElement).style.removeProperty('height')
|
||||||
|
;(el as HTMLElement).style.removeProperty('opacity');
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitValue(value: any) {
|
||||||
|
emits('update:activeKey', value);
|
||||||
|
emits('change', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(key: number | string) {
|
||||||
|
if (activeCheck(key)) {
|
||||||
|
if (Array.isArray(props.activeKey)) {
|
||||||
|
const res = (props.activeKey as any[]).filter((actKey: number | string) => actKey !== key);
|
||||||
|
emitValue(res);
|
||||||
|
} else {
|
||||||
|
emitValue(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Array.isArray(props.activeKey)) {
|
||||||
|
emitValue([...props.activeKey, key]);
|
||||||
|
} else {
|
||||||
|
emitValue(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeCheck(key: number | string): boolean {
|
||||||
|
if (Array.isArray(props.activeKey)) {
|
||||||
|
return (props.activeKey as any[]).includes(key);
|
||||||
|
} else {
|
||||||
|
return props.activeKey === key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCopy(index: number) {
|
||||||
|
navigator.clipboard.writeText(contentRef.value[index].innerText || '').then(
|
||||||
|
() => {
|
||||||
|
/* clipboard successfully set */
|
||||||
|
copyTxt.value = 'Copied';
|
||||||
|
rafTimeout(() => {
|
||||||
|
copyTxt.value = 'Copy';
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
/* clipboard write failed */
|
||||||
|
copyTxt.value = err;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-collapse"
|
||||||
|
:class="{
|
||||||
|
'collapse-borderless': !bordered,
|
||||||
|
'collapse-arrow-right': arrowPlacement === 'right',
|
||||||
|
'collapse-ghost': ghost
|
||||||
|
}"
|
||||||
|
:style="collapseStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="m-collapse-item"
|
||||||
|
:class="{ 'collapse-item-disabled': data.disabled === undefined ? disabled : data.disabled }"
|
||||||
|
:style="itemStyle"
|
||||||
|
v-for="(data, index) in collapseData"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="m-collapse-header"
|
||||||
|
:class="{ 'collapse-header-no-arrow': data.showArrow !== undefined ? !data.showArrow : !showArrow }"
|
||||||
|
:style="headerStyle"
|
||||||
|
@click="(data.disabled === undefined ? disabled : data.disabled) ? () => false : onClick(data.key || index)"
|
||||||
|
@keydown.enter="onClick(data.key || index)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="data.showArrow !== undefined ? data.showArrow : showArrow"
|
||||||
|
class="collapse-arrow"
|
||||||
|
:style="arrowStyle"
|
||||||
|
>
|
||||||
|
<slot name="arrow" :key="data.key || index" :active="activeCheck(data.key || index)">
|
||||||
|
<svg
|
||||||
|
class="arrow-svg"
|
||||||
|
:class="{ 'arrow-rotate': activeCheck(data.key || index) }"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="right"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-header">
|
||||||
|
<slot name="header" :header="data.header" :key="data.key || index" :active="activeCheck(data.key || index)">
|
||||||
|
{{ data.header || '--' }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-extra">
|
||||||
|
<slot name="extra" :extra="data.extra" :key="data.key || index" :active="activeCheck(data.key || index)">
|
||||||
|
{{ data.extra || extra }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
name="collapse"
|
||||||
|
@enter="setProperties"
|
||||||
|
@after-enter="removeProperties"
|
||||||
|
@leave="setProperties"
|
||||||
|
@after-leave="removeProperties"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="activeCheck(data.key || index)"
|
||||||
|
class="m-collapse-content"
|
||||||
|
:class="{ 'collapse-copyable': copyable }"
|
||||||
|
>
|
||||||
|
<div class="collapse-lang">
|
||||||
|
<slot name="lang" :lang="lang" :key="data.key || index" :active="activeCheck(data.key || index)">
|
||||||
|
{{ lang }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<Button class="collapse-copy" size="small" type="primary" @click="onCopy(index)" v-bind="copyProps">{{
|
||||||
|
copyTxt
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
<div ref="contentRef" class="collapse-content" :style="contentStyle">
|
||||||
|
<slot
|
||||||
|
name="content"
|
||||||
|
:content="data.content"
|
||||||
|
:key="data.key || index"
|
||||||
|
:active="activeCheck(data.key || index)"
|
||||||
|
>
|
||||||
|
{{ data.content }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.collapse-enter-active,
|
||||||
|
.collapse-leave-active {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height 0.2s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||||
|
opacity 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-enter-from,
|
||||||
|
.collapse-leave-to {
|
||||||
|
height: 0 !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-collapse {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.m-collapse-item {
|
||||||
|
border-bottom: 1px solid #d9d9d9;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
|
||||||
|
.m-collapse-header,
|
||||||
|
.m-collapse-content {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-collapse-header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 12px;
|
||||||
|
|
||||||
|
.arrow-rotate {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-header {
|
||||||
|
// 元素会根据自身的宽度与高度来确定尺寸,但是会伸长并吸收 flex 容器中额外的自由空间,也会缩短自身来适应 flex 容器
|
||||||
|
flex: auto; // 相当于 flex: 1 1 auto
|
||||||
|
margin-right: auto;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-extra {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-header-no-arrow {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-collapse-content {
|
||||||
|
position: relative;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-top: 1px solid #d9d9d9;
|
||||||
|
|
||||||
|
.collapse-lang {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.38);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-copy {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 8px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-content {
|
||||||
|
padding: 16px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-copyable {
|
||||||
|
&:hover {
|
||||||
|
.collapse-lang {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-copy {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-item-disabled {
|
||||||
|
.m-collapse-header {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-borderless {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
.m-collapse-item {
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
|
||||||
|
.m-collapse-header {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-collapse-content {
|
||||||
|
background-color: transparent;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-arrow-right {
|
||||||
|
.m-collapse-item .m-collapse-header .collapse-arrow {
|
||||||
|
order: 1; // order 属性定义项目的排列顺序。数值越小,排列越靠前,默认为 0
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-ghost {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
.m-collapse-item {
|
||||||
|
border-bottom: 0;
|
||||||
|
|
||||||
|
.m-collapse-content {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
250
src/components/MyUI/Countdown/Countdown.vue
Normal file
250
src/components/MyUI/Countdown/Countdown.vue
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed, onMounted, ref, watch} from 'vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string // 倒计时标题 string | slot
|
||||||
|
titleStyle?: CSSProperties // 设置标题的样式
|
||||||
|
prefix?: string // 倒计时的前缀 string | slot
|
||||||
|
suffix?: string // 倒计时的后缀 string | slot
|
||||||
|
finishedText?: string // 完成后的展示文本 string | slot
|
||||||
|
future?: boolean // value 是否为未来某时刻的时间戳;为 false 表示相对剩余时间戳
|
||||||
|
format?: string // 倒计时展示格式,(Y/YY:年,M/MM:月,D/DD:日,H/HH:时,m/mm:分钟,s/ss:秒,SSS:毫秒)
|
||||||
|
value?: number // 倒计时数值,支持设置未来某时刻的时间戳 (ms) 或 相对剩余时间 (ms)
|
||||||
|
valueStyle?: CSSProperties // 设置倒计时的样式
|
||||||
|
active?: boolean // 是否处于计时状态,仅当 future: false 时生效
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: undefined,
|
||||||
|
titleStyle: () => ({}),
|
||||||
|
prefix: undefined,
|
||||||
|
suffix: undefined,
|
||||||
|
finishedText: undefined,
|
||||||
|
future: true,
|
||||||
|
format: 'HH:mm:ss',
|
||||||
|
value: 0,
|
||||||
|
valueStyle: () => ({}),
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
const futureTime = ref(0); // 未来截止时间戳
|
||||||
|
const remainingTime = ref(0); // 剩余时间戳
|
||||||
|
const rafID = ref<number | null>(null); // requestAnimationFrame 返回的请求 ID 是一个 long 类型整数值,是在回调列表里的唯一标识符
|
||||||
|
const emit = defineEmits(['finish']);
|
||||||
|
const slotsExist = useSlotsExist(['title', 'prefix', 'suffix']);
|
||||||
|
const showTitle = computed(() => {
|
||||||
|
return slotsExist.title || props.title;
|
||||||
|
});
|
||||||
|
const showPrefix = computed(() => {
|
||||||
|
return slotsExist.prefix || props.prefix;
|
||||||
|
});
|
||||||
|
const showSuffix = computed(() => {
|
||||||
|
return slotsExist.suffix || props.suffix;
|
||||||
|
});
|
||||||
|
const showType = computed(() => {
|
||||||
|
return {
|
||||||
|
showMillisecond: props.format.includes('SSS'),
|
||||||
|
showYear: props.format.includes('Y'),
|
||||||
|
showMonth: props.format.includes('M'),
|
||||||
|
showDay: props.format.includes('D'),
|
||||||
|
showHour: props.format.includes('H'),
|
||||||
|
showMinute: props.format.includes('m'),
|
||||||
|
showSecond: props.format.includes('s')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.active,
|
||||||
|
(to: boolean) => {
|
||||||
|
if (!props.future) {
|
||||||
|
if (to) {
|
||||||
|
futureTime.value = remainingTime.value + Date.now();
|
||||||
|
rafID.value = requestAnimationFrame(CountDown);
|
||||||
|
} else {
|
||||||
|
if (rafID.value) {
|
||||||
|
cancelAnimationFrame(rafID.value);
|
||||||
|
}
|
||||||
|
rafID.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => [props.value, props.future],
|
||||||
|
() => {
|
||||||
|
initCountdown();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
onMounted(() => {
|
||||||
|
initCountdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
function initCountdown() {
|
||||||
|
// 只有数值类型的值,且是有穷的(finite),才返回 true
|
||||||
|
if (Number.isFinite(props.value)) {
|
||||||
|
// 检测传入的参数是否是一个有穷数
|
||||||
|
if (props.future) {
|
||||||
|
// 未来某时刻的时间戳,单位ms
|
||||||
|
if (props.value > Date.now()) {
|
||||||
|
futureTime.value = props.value;
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 相对剩余时间,单位 ms
|
||||||
|
if (props.value > 0) {
|
||||||
|
futureTime.value = props.value + Date.now();
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remainingTime.value = futureTime.value - Date.now();
|
||||||
|
if (props.future || (!props.future && props.active)) {
|
||||||
|
if (rafID.value) {
|
||||||
|
cancelAnimationFrame(rafID.value);
|
||||||
|
}
|
||||||
|
rafID.value = requestAnimationFrame(CountDown);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remainingTime.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
remainingTime.value = 0;
|
||||||
|
emit('finish');
|
||||||
|
}
|
||||||
|
|
||||||
|
function CountDown() {
|
||||||
|
if (futureTime.value > Date.now()) {
|
||||||
|
remainingTime.value = futureTime.value - Date.now();
|
||||||
|
rafID.value = requestAnimationFrame(CountDown);
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前置补 0
|
||||||
|
function padZero(value: number, targetLength: number = 2): string {
|
||||||
|
// 左侧补零函数
|
||||||
|
return String(value).padStart(targetLength, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeFormat(time: number): string {
|
||||||
|
let showTime = props.format;
|
||||||
|
if (showType.value.showMillisecond) {
|
||||||
|
var millisecond = time % 1000;
|
||||||
|
showTime = showTime.replace('SSS', padZero(millisecond, 3));
|
||||||
|
}
|
||||||
|
time = Math.floor(time / 1000); // 将时间转为 s 为单位
|
||||||
|
let Y: number, M: number, D: number, H: number, m: number, s: number;
|
||||||
|
if (showType.value.showYear) {
|
||||||
|
Y = Math.floor(time / (60 * 60 * 24 * 30 * 12));
|
||||||
|
showTime = showTime.includes('YY') ? showTime.replace('YY', padZero(Y)) : showTime.replace('Y', String(Y));
|
||||||
|
} else {
|
||||||
|
Y = 0;
|
||||||
|
}
|
||||||
|
if (showType.value.showMonth) {
|
||||||
|
time = time - Y * 60 * 60 * 24 * 30 * 12;
|
||||||
|
M = Math.floor(time / (60 * 60 * 24 * 30));
|
||||||
|
showTime = showTime.includes('MM') ? showTime.replace('MM', padZero(M)) : showTime.replace('M', String(M));
|
||||||
|
} else {
|
||||||
|
M = 0;
|
||||||
|
}
|
||||||
|
if (showType.value.showDay) {
|
||||||
|
time = time - M * 60 * 60 * 24 * 30;
|
||||||
|
D = Math.floor(time / (60 * 60 * 24));
|
||||||
|
showTime = showTime.includes('DD') ? showTime.replace('DD', padZero(D)) : showTime.replace('D', String(D));
|
||||||
|
} else {
|
||||||
|
D = 0;
|
||||||
|
}
|
||||||
|
if (showType.value.showHour) {
|
||||||
|
time = time - D * 60 * 60 * 24;
|
||||||
|
H = Math.floor(time / (60 * 60));
|
||||||
|
showTime = showTime.includes('HH') ? showTime.replace('HH', padZero(H)) : showTime.replace('H', String(H));
|
||||||
|
} else {
|
||||||
|
H = 0;
|
||||||
|
}
|
||||||
|
if (showType.value.showMinute) {
|
||||||
|
time = time - H * 60 * 60;
|
||||||
|
m = Math.floor(time / 60);
|
||||||
|
showTime = showTime.includes('mm') ? showTime.replace('mm', padZero(m)) : showTime.replace('m', String(m));
|
||||||
|
} else {
|
||||||
|
m = 0;
|
||||||
|
}
|
||||||
|
if (showType.value.showSecond) {
|
||||||
|
s = time - m * 60;
|
||||||
|
showTime = showTime.includes('ss') ? showTime.replace('ss', padZero(s)) : showTime.replace('s', String(s));
|
||||||
|
}
|
||||||
|
return showTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCountdown() {
|
||||||
|
// 重置倒计时
|
||||||
|
initCountdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reset: resetCountdown
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-countdown">
|
||||||
|
<div v-if="showTitle" class="countdown-title" :style="titleStyle">
|
||||||
|
<slot name="title">{{ props.title }}</slot>
|
||||||
|
</div>
|
||||||
|
<div class="countdown-time">
|
||||||
|
<template v-if="showPrefix">
|
||||||
|
<span class="time-prefix" v-if="showPrefix || remainingTime > 0">
|
||||||
|
<slot name="prefix">{{ prefix }}</slot>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-if="finishedText && remainingTime === 0" class="time-value" :style="valueStyle">
|
||||||
|
<slot name="finish">{{ finishedText }}</slot>
|
||||||
|
</span>
|
||||||
|
<span v-else class="time-value" :style="valueStyle">
|
||||||
|
{{ timeFormat(remainingTime) }}
|
||||||
|
</span>
|
||||||
|
<template v-if="showSuffix">
|
||||||
|
<span class="time-suffix" v-if="showSuffix || remainingTime > 0">
|
||||||
|
<slot name="suffix">{{ suffix }}</slot>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-countdown {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
|
||||||
|
.countdown-title {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-time {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: 'Helvetica Neue'; // 保证数字等宽显示
|
||||||
|
.time-prefix {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
display: inline-block;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-suffix {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
src/components/MyUI/DatePicker/DatePicker.vue
Normal file
124
src/components/MyUI/DatePicker/DatePicker.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import VueDatePicker from '@vuepic/vue-datepicker';
|
||||||
|
import '@vuepic/vue-datepicker/dist/main.css';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
interface Props {
|
||||||
|
width?: number // 日期选择器宽度
|
||||||
|
mode?: 'time' | 'date' | 'week' | 'month' | 'year' // 选择器模式,可选:时间time,日期date,周week,月month,年year
|
||||||
|
// format?: string | ((date: Date) => string) | ((dates: Date[]) => string) // 日期展示格式,(yy: 年, M: 月, d: 天, H: 时, m: 分, s: 秒, w: 周)
|
||||||
|
showTime?: boolean // 是否增加时间选择
|
||||||
|
showToday?: boolean // 是否展示”今天“按钮
|
||||||
|
// multiCalendars?: boolean // 范围选择器是否使用双日期面板
|
||||||
|
// flow?: any[] // 定义选择顺序 ("calendar" | "time" | "month" | "year" | "minutes" | "hours" | "seconds")[]
|
||||||
|
// dark?: boolean // 样式主题是否使用黑色
|
||||||
|
modelType?: 'timestamp' | 'format' // v-model 值类型,可选 timestamp: 时间戳、format: 字符串,mode 为 week 或 year 时,该配置不生效
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 180,
|
||||||
|
mode: 'date',
|
||||||
|
/* format default
|
||||||
|
Single picker: 'MM/dd/yyyy HH:mm'
|
||||||
|
Range picker: 'MM/dd/yyyy HH:mm - MM/dd/yyyy HH:mm'
|
||||||
|
Month picker: 'MM/yyyy'
|
||||||
|
Time picker: 'HH:mm'
|
||||||
|
Time picker range: 'HH:mm - HH:mm'
|
||||||
|
Week picker: 'ww-yyyy'
|
||||||
|
*/
|
||||||
|
showTime: false,
|
||||||
|
showToday: false,
|
||||||
|
// multiCalendars: false,
|
||||||
|
// flow: () => [],
|
||||||
|
// dark: false,
|
||||||
|
modelType: 'format'
|
||||||
|
});
|
||||||
|
const time = computed(() => {
|
||||||
|
return props.mode === 'time';
|
||||||
|
});
|
||||||
|
const week = computed(() => {
|
||||||
|
return props.mode === 'week';
|
||||||
|
});
|
||||||
|
const month = computed(() => {
|
||||||
|
return props.mode === 'month';
|
||||||
|
});
|
||||||
|
const year = computed(() => {
|
||||||
|
return props.mode === 'year';
|
||||||
|
});
|
||||||
|
// const format = (date: Date) => {
|
||||||
|
// const day = date.getDate()
|
||||||
|
// const month = date.getMonth() + 1
|
||||||
|
// const year = date.getFullYear()
|
||||||
|
// return `${year}-${month}-${day}`
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VueDatePicker
|
||||||
|
class="m-datepicker" :style="`width: ${width}px;`"
|
||||||
|
locale="zh-CN"
|
||||||
|
:month-change-on-scroll="false"
|
||||||
|
:enable-time-picker="showTime"
|
||||||
|
:time-picker="time"
|
||||||
|
:week-picker="week"
|
||||||
|
:month-picker="month"
|
||||||
|
:year-picker="year"
|
||||||
|
now-button-label="今天"
|
||||||
|
:show-now-button="showToday"
|
||||||
|
auto-apply
|
||||||
|
text-input
|
||||||
|
:model-type="modelType"
|
||||||
|
:day-names="['一', '二', '三', '四', '五', '六', '七']"
|
||||||
|
></VueDatePicker>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-datepicker {
|
||||||
|
display: inline-block;
|
||||||
|
:deep(.dp__input_wrap) {
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dp__theme_dark {
|
||||||
|
// dark theme
|
||||||
|
--dp-background-color: #212121;
|
||||||
|
--dp-text-color: #ffffff;
|
||||||
|
--dp-hover-color: #484848;
|
||||||
|
--dp-hover-text-color: #ffffff;
|
||||||
|
--dp-hover-icon-color: #959595;
|
||||||
|
--dp-primary-color: #005cb2;
|
||||||
|
--dp-primary-text-color: #ffffff;
|
||||||
|
--dp-secondary-color: #a9a9a9;
|
||||||
|
--dp-border-color: #2d2d2d;
|
||||||
|
--dp-menu-border-color: #2d2d2d;
|
||||||
|
--dp-border-color-hover: #aaaeb7;
|
||||||
|
--dp-disabled-color: #737373;
|
||||||
|
--dp-scroll-bar-background: #212121;
|
||||||
|
--dp-scroll-bar-color: #484848;
|
||||||
|
--dp-success-color: #00701a;
|
||||||
|
--dp-success-color-disabled: #428f59;
|
||||||
|
--dp-icon-color: #959595;
|
||||||
|
--dp-danger-color: #e53935;
|
||||||
|
--dp-highlight-color: rgba(0, 92, 178, 0.2);
|
||||||
|
}
|
||||||
|
.dp__theme_light {
|
||||||
|
// light theme
|
||||||
|
--dp-background-color: #ffffff;
|
||||||
|
--dp-text-color: #212121;
|
||||||
|
--dp-hover-color: #f3f3f3;
|
||||||
|
--dp-hover-text-color: #212121;
|
||||||
|
--dp-hover-icon-color: #959595;
|
||||||
|
--dp-primary-color: #1976d2;
|
||||||
|
--dp-primary-text-color: #f8f5f5;
|
||||||
|
--dp-secondary-color: #c0c4cc;
|
||||||
|
--dp-border-color: #ddd;
|
||||||
|
--dp-menu-border-color: #ddd;
|
||||||
|
--dp-border-color-hover: #aaaeb7;
|
||||||
|
--dp-disabled-color: #f6f6f6;
|
||||||
|
--dp-scroll-bar-background: #f3f3f3;
|
||||||
|
--dp-scroll-bar-color: #959595;
|
||||||
|
--dp-success-color: #76d275;
|
||||||
|
--dp-success-color-disabled: #a3d9b1;
|
||||||
|
--dp-icon-color: #959595;
|
||||||
|
--dp-danger-color: #ff6f60;
|
||||||
|
--dp-highlight-color: rgba(25, 118, 210, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
437
src/components/MyUI/Descriptions/Descriptions.vue
Normal file
437
src/components/MyUI/Descriptions/Descriptions.vue
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed, nextTick, onMounted, ref, watch} from 'vue';
|
||||||
|
import {useEventListener, useMutationObserver, useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Responsive {
|
||||||
|
xs?: number // <576px 响应式栅格
|
||||||
|
sm?: number // ≥576px 响应式栅格
|
||||||
|
md?: number // ≥768px 响应式栅格
|
||||||
|
lg?: number // ≥992px 响应式栅格
|
||||||
|
xl?: number // ≥1200px 响应式栅格
|
||||||
|
xxl?: number // ≥1600px 响应式栅格
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string // 描述列表的标题,显示在最顶部 string | slot
|
||||||
|
extra?: string // 描述列表的操作区域,显示在右上方 string | slot
|
||||||
|
bordered?: boolean // 是否展示边框
|
||||||
|
vertical?: boolean // 是否使用垂直描述列表
|
||||||
|
size?: 'default' | 'middle' | 'small' // 设置列表的大小
|
||||||
|
column?: number | Responsive // 一行的 DescriptionItems 数量,可以写成数值或支持响应式的对象写法 { xs: 8, sm: 16, md: 24 }
|
||||||
|
labelStyle?: CSSProperties // 自定义标签样式,优先级低于 DescriptionItems 的 labelStyle
|
||||||
|
contentStyle?: CSSProperties // 自定义内容样式,优先级低于 DescriptionItems 的 contentStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: undefined,
|
||||||
|
extra: undefined,
|
||||||
|
bordered: false,
|
||||||
|
vertical: false,
|
||||||
|
size: 'default',
|
||||||
|
column: () => ({xs: 1, sm: 2, md: 3}),
|
||||||
|
labelStyle: () => ({}),
|
||||||
|
contentStyle: () => ({})
|
||||||
|
});
|
||||||
|
const defaultSlotsRef = ref(); // 所有渲染的 DescriptionsItems 节点引用
|
||||||
|
const defaultSlots = ref(true); // 用于刷新 <slot></slot>
|
||||||
|
const stopObservation = ref(true); // 停止观察器
|
||||||
|
const children = ref<any[]>(); // DescriptionsItems 节点
|
||||||
|
const tdCols = ref(); // 放置 DescriptionsItems 节点的模板引用数组
|
||||||
|
const thVerticalCols = ref(); // 放置垂直列表的 DescriptionsItems 节点的 th 模板引用数组
|
||||||
|
const tdVerticalCols = ref(); // 放置垂直列表的 DescriptionsItems 节点的 td 模板引用数组
|
||||||
|
const trBorderedRows = ref(); // 放置 DescriptionsItems 节点的模板引用数组(带边框)
|
||||||
|
const thVerticalBorderedRows = ref(); // 放置垂直列表的 DescriptionsItems 节点的 th 模板引用数组(带边框)
|
||||||
|
const tdVerticalBorderedRows = ref(); // 放置垂直列表的 DescriptionsItems 节点的 td 模板引用数组(带边框)
|
||||||
|
const groupItems = ref<any[]>([]); // 处理后的 DescriptionsItems 节点数组
|
||||||
|
const viewportWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
|
function getViewportWidth() {
|
||||||
|
viewportWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener(window, 'resize', getViewportWidth);
|
||||||
|
const slotsExist = useSlotsExist(['title', 'extra']);
|
||||||
|
const showHeader = computed(() => {
|
||||||
|
return slotsExist.title || slotsExist.extra || props.title || props.extra;
|
||||||
|
});
|
||||||
|
const responsiveColumn = computed(() => {
|
||||||
|
if (typeof props.column === 'object') {
|
||||||
|
if (viewportWidth.value >= 1600 && props.column.xxl) {
|
||||||
|
return props.column.xxl;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value >= 1200 && props.column.xl) {
|
||||||
|
return props.column.xl;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value >= 992 && props.column.lg) {
|
||||||
|
return props.column.lg;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value >= 768 && props.column.md) {
|
||||||
|
return props.column.md;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value >= 576 && props.column.sm) {
|
||||||
|
return props.column.sm;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value < 576 && props.column.xs) {
|
||||||
|
return props.column.xs;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return props.column;
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => [props.bordered, props.vertical, responsiveColumn.value, props.labelStyle, props.contentStyle],
|
||||||
|
() => {
|
||||||
|
if (!stopObservation.value) {
|
||||||
|
stopObservation.value = true;
|
||||||
|
}
|
||||||
|
refreshDefaultSlots();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 监听 defaultSlotsRef DOM 节点数量变化,重新渲染 Descriptions
|
||||||
|
useMutationObserver(
|
||||||
|
defaultSlotsRef,
|
||||||
|
(MutationRecord: MutationRecord[]) => {
|
||||||
|
if (!stopObservation.value) {
|
||||||
|
stopObservation.value = true;
|
||||||
|
const mutation = MutationRecord.some((mutation: any) => mutation.type === 'childList');
|
||||||
|
if (mutation) {
|
||||||
|
refreshDefaultSlots();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{subtree: true, childList: true, attributes: true}
|
||||||
|
);
|
||||||
|
onMounted(() => {
|
||||||
|
getGroupItems();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refreshDefaultSlots() {
|
||||||
|
defaultSlots.value = !defaultSlots.value;
|
||||||
|
await nextTick();
|
||||||
|
getGroupItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前 group 中所有 span 之和
|
||||||
|
function getTotalSpan(group: any): number {
|
||||||
|
return group.reduce((accumulator: number, currentValue: any) => accumulator + currentValue.span, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据不同 cloumn 处理 DescriptionsItems 节点
|
||||||
|
async function getGroupItems() {
|
||||||
|
children.value = Array.from(defaultSlotsRef.value.children).filter((element: any) => {
|
||||||
|
return element.className === (props.bordered ? 'descriptions-item-bordered' : 'descriptions-item');
|
||||||
|
});
|
||||||
|
if (groupItems.value.length) {
|
||||||
|
groupItems.value.splice(0); // 清空列表
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
if (children.value && children.value.length) {
|
||||||
|
const len = children.value.length;
|
||||||
|
let group: any[] = [];
|
||||||
|
for (let n = 0; n < len; n++) {
|
||||||
|
const item = {
|
||||||
|
span: Math.min(children.value[n].dataset.span ?? 1, responsiveColumn.value),
|
||||||
|
element: children.value[n]
|
||||||
|
};
|
||||||
|
if (getTotalSpan(group) < responsiveColumn.value) {
|
||||||
|
// 已有 items 的 totalSpan < column
|
||||||
|
item.span = Math.min(item.span, responsiveColumn.value - getTotalSpan(group));
|
||||||
|
group.push(item);
|
||||||
|
} else {
|
||||||
|
groupItems.value.push(group);
|
||||||
|
group = [item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 当使用水平列表且未设置 span 时等效于 span: 1,但最后一行的最后一项,会包含该行剩余的所有列数
|
||||||
|
if (!props.vertical && !children.value[len - 1].dataset.span && getTotalSpan(group) < responsiveColumn.value) {
|
||||||
|
const groupLen = group.length;
|
||||||
|
group[groupLen - 1].span = group[groupLen - 1].span + responsiveColumn.value - getTotalSpan(group);
|
||||||
|
}
|
||||||
|
groupItems.value.push(group);
|
||||||
|
await nextTick();
|
||||||
|
updateDescriptions();
|
||||||
|
} else {
|
||||||
|
stopObservation.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDescriptions() {
|
||||||
|
if (props.bordered) {
|
||||||
|
// 带边框列表
|
||||||
|
groupItems.value.forEach((items: any, index: number) => {
|
||||||
|
// 每一行 tr
|
||||||
|
items.forEach((item: any) => {
|
||||||
|
const itemChildren: any[] = Array.from(item.element.children);
|
||||||
|
// 创建节点副本,否则原节点将先被移除,后插入到新位置,影响后续响应式布局计算
|
||||||
|
const th = itemChildren[0];
|
||||||
|
// 动态添加节点样式
|
||||||
|
setStyle(th, props.labelStyle);
|
||||||
|
const td = itemChildren[1];
|
||||||
|
// 动态添加节点样式
|
||||||
|
setStyle(td, props.contentStyle);
|
||||||
|
// 插入节点到指定位置
|
||||||
|
if (props.vertical) {
|
||||||
|
// 垂直列表
|
||||||
|
th.colSpan = item.span;
|
||||||
|
td.colSpan = item.span;
|
||||||
|
thVerticalBorderedRows.value[index].appendChild(th);
|
||||||
|
tdVerticalBorderedRows.value[index].appendChild(td);
|
||||||
|
} else {
|
||||||
|
th.colSpan = 1;
|
||||||
|
td.colSpan = item.span * 2 - 1;
|
||||||
|
trBorderedRows.value[index].appendChild(th);
|
||||||
|
trBorderedRows.value[index].appendChild(td);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
;(children.value as any[]).forEach((element: any, index: number) => {
|
||||||
|
const elementChildren: any[] = Array.from(element.children);
|
||||||
|
const label = elementChildren[0];
|
||||||
|
// 动态添加节点样式
|
||||||
|
setStyle(label, props.labelStyle);
|
||||||
|
const content = elementChildren[1];
|
||||||
|
// 动态添加节点样式
|
||||||
|
setStyle(content, props.contentStyle);
|
||||||
|
// 插入节点到指定位置
|
||||||
|
if (props.vertical) {
|
||||||
|
// 垂直列表
|
||||||
|
thVerticalCols.value[index].appendChild(element.firstChild);
|
||||||
|
tdVerticalCols.value[index].appendChild(element.lastChild);
|
||||||
|
} else {
|
||||||
|
tdCols.value[index].appendChild(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
stopObservation.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为元素添加内联样式
|
||||||
|
function setStyle(element: any, styles: any) {
|
||||||
|
if (JSON.stringify(styles) !== '{}') {
|
||||||
|
Object.keys(styles).forEach((key: string) => {
|
||||||
|
if (!element.style[key]) {
|
||||||
|
element.style[key] = styles[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-descriptions" :class="`descriptions-${size}`">
|
||||||
|
<div class="m-descriptions-header" v-if="showHeader">
|
||||||
|
<div class="descriptions-title">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</div>
|
||||||
|
<div class="descriptions-extra">
|
||||||
|
<slot name="extra">{{ extra }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!vertical" class="m-descriptions-view" :class="{ 'descriptions-bordered': bordered }">
|
||||||
|
<table>
|
||||||
|
<tbody v-if="!bordered">
|
||||||
|
<tr v-for="(items, row) in groupItems" :key="row">
|
||||||
|
<td
|
||||||
|
ref="tdCols"
|
||||||
|
class="descriptions-item-td"
|
||||||
|
:colspan="item.span"
|
||||||
|
v-for="(item, col) in items"
|
||||||
|
:key="col"
|
||||||
|
></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tbody v-else>
|
||||||
|
<tr ref="trBorderedRows" class="descriptions-bordered-tr" v-for="row of groupItems.length" :key="row"></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div v-else class="m-descriptions-view" :class="{ 'descriptions-bordered': bordered }">
|
||||||
|
<table>
|
||||||
|
<tbody v-if="!bordered">
|
||||||
|
<template v-for="(items, row) in groupItems" :key="row">
|
||||||
|
<tr>
|
||||||
|
<th class="descriptions-item-th" :colspan="item.span" v-for="(item, col) in items" :key="col">
|
||||||
|
<div ref="thVerticalCols" class="descriptions-item"></div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="descriptions-item-td" :colspan="item.span" v-for="(item, col) in items" :key="col">
|
||||||
|
<div ref="tdVerticalCols" class="descriptions-item"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
<tbody v-else>
|
||||||
|
<template v-for="row in groupItems.length" :key="row">
|
||||||
|
<tr ref="thVerticalBorderedRows" class="descriptions-bordered-tr"></tr>
|
||||||
|
<tr ref="tdVerticalBorderedRows" class="descriptions-bordered-tr"></tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div ref="defaultSlotsRef" v-show="false">
|
||||||
|
<slot v-if="defaultSlots"></slot>
|
||||||
|
<slot v-else></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-descriptions {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
|
||||||
|
.m-descriptions-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.descriptions-title {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: auto;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-extra {
|
||||||
|
margin-left: auto;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-descriptions-view {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
display: table; // 可选,只为兼容 vitepress 中 .vp-doc 的样式入侵,下同
|
||||||
|
border-collapse: separate; // 可选
|
||||||
|
margin: 0; // 可选
|
||||||
|
tr {
|
||||||
|
// 可选
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-item-th {
|
||||||
|
padding: 0; // 可选
|
||||||
|
border: none; // 可选
|
||||||
|
padding-bottom: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
background: transparent; // 可选
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-item-td {
|
||||||
|
padding: 0; // 可选
|
||||||
|
border: none; // 可选
|
||||||
|
padding-bottom: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-bordered {
|
||||||
|
border: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
|
||||||
|
table {
|
||||||
|
table-layout: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
display: table; // 可选
|
||||||
|
margin: 0; // 可选
|
||||||
|
.descriptions-bordered-tr {
|
||||||
|
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.descriptions-label-th) {
|
||||||
|
border: none; // 可选
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
text-align: start;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-right: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
// 消除 vertical 列表最后一个 th 的边框
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.descriptions-content-td) {
|
||||||
|
border: none; // 可选
|
||||||
|
display: table-cell;
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-right: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-middle {
|
||||||
|
.m-descriptions-view {
|
||||||
|
.descriptions-item-td {
|
||||||
|
padding-bottom: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-bordered {
|
||||||
|
:deep(.descriptions-label-th) {
|
||||||
|
padding: 12px 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.descriptions-content-td) {
|
||||||
|
padding: 12px 24px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-small {
|
||||||
|
.m-descriptions-view {
|
||||||
|
.descriptions-item-td {
|
||||||
|
padding-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptions-bordered {
|
||||||
|
:deep(.descriptions-label-th) {
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.descriptions-content-td) {
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
493
src/components/MyUI/Dialog/Dialog.vue
Normal file
493
src/components/MyUI/Dialog/Dialog.vue
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed, nextTick, onMounted, onUnmounted, ref, watch, watchEffect} from 'vue';
|
||||||
|
import Button from '../Button/Button.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 对话框宽度,单位 px
|
||||||
|
height?: string | number // 对话框高度,单位 px,默认自适应内容高度
|
||||||
|
title?: string // 标题 string | slot
|
||||||
|
titleStyle?: CSSProperties // 自定义标题样式
|
||||||
|
content?: string // 内容 string | slot
|
||||||
|
contentStyle?: CSSProperties // 自定义内容样式
|
||||||
|
bodyClass?: string // 自定义 body 类名
|
||||||
|
bodyStyle?: CSSProperties // 自定义 body 样式
|
||||||
|
cancelText?: string // 取消按钮文字
|
||||||
|
cancelProps?: object // 取消按钮 props 配置,参考 Button 组件 Props
|
||||||
|
okText?: string // 确定按钮文字
|
||||||
|
okType?: 'primary' | 'danger' // 确定按钮类型
|
||||||
|
okProps?: object // 确认按钮 props 配置,优先级高于 okType,参考 Button 组件 Props
|
||||||
|
footer?: boolean // 是否显示底部按钮 boolean | slot
|
||||||
|
switchFullscreen?: boolean // 是否允许切换全屏,允许后右上角会出现一个切换按钮
|
||||||
|
centered?: boolean // 是否水平垂直居中,否则固定高度水平居中
|
||||||
|
top?: string | number // 固定高度水平居中时,距顶部高度,仅当 centered: false 时生效,单位 px
|
||||||
|
transformOrigin?: 'mouse' | 'center' // 对话框动画出现的位置
|
||||||
|
confirmLoading?: boolean // 确定按钮 loading
|
||||||
|
blockScroll?: boolean // 是否在打开对话框时禁用背景滚动
|
||||||
|
keyboard?: boolean // 是否支持键盘 esc 关闭
|
||||||
|
maskClosable?: boolean // 点击蒙层是否允许关闭
|
||||||
|
maskStyle?: CSSProperties // 自定义蒙层样式
|
||||||
|
open?: boolean // 对话框是否可见
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 520,
|
||||||
|
height: 'auto',
|
||||||
|
title: undefined,
|
||||||
|
titleStyle: () => ({}),
|
||||||
|
content: undefined,
|
||||||
|
contentStyle: () => ({}),
|
||||||
|
bodyClass: undefined,
|
||||||
|
bodyStyle: () => ({}),
|
||||||
|
cancelText: '取消',
|
||||||
|
cancelProps: () => ({}),
|
||||||
|
okText: '确定',
|
||||||
|
okType: 'primary',
|
||||||
|
okProps: () => ({}),
|
||||||
|
footer: true,
|
||||||
|
switchFullscreen: false,
|
||||||
|
centered: false,
|
||||||
|
top: 100,
|
||||||
|
transformOrigin: 'mouse',
|
||||||
|
confirmLoading: false,
|
||||||
|
blockScroll: true,
|
||||||
|
keyboard: true,
|
||||||
|
maskClosable: true,
|
||||||
|
maskStyle: () => ({}),
|
||||||
|
open: false
|
||||||
|
});
|
||||||
|
const dialogRef = ref(); // dialog DOM 引用
|
||||||
|
const mousePosition = ref<{ x: number; y: number } | null>(null); // 鼠标点击位置
|
||||||
|
const dialogOpen = ref<boolean>();
|
||||||
|
const showDialogWrap = ref<boolean>();
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
const transformOrigin = ref<string>('50% 50%');
|
||||||
|
const fullscreen = ref<boolean>(false);
|
||||||
|
const emits = defineEmits(['update:open', 'cancel', 'ok']);
|
||||||
|
const dialogWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const dialogHeight = computed(() => {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
}
|
||||||
|
return props.height;
|
||||||
|
});
|
||||||
|
const dialogTop = computed(() => {
|
||||||
|
if (typeof props.top === 'number') {
|
||||||
|
return `${props.top}px`;
|
||||||
|
}
|
||||||
|
return props.top;
|
||||||
|
});
|
||||||
|
const dialogStyle = computed(() => {
|
||||||
|
if (fullscreen.value) {
|
||||||
|
if (props.transformOrigin === 'mouse') {
|
||||||
|
return {
|
||||||
|
width: '100%',
|
||||||
|
transformOrigin: `${mousePosition.value?.x}px ${mousePosition.value?.y}px`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
width: '100%',
|
||||||
|
transformOrigin: transformOrigin.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (props.centered) {
|
||||||
|
return {
|
||||||
|
width: dialogWidth.value,
|
||||||
|
transformOrigin: transformOrigin.value
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
width: dialogWidth.value,
|
||||||
|
transformOrigin: transformOrigin.value,
|
||||||
|
top: dialogTop.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const dialogBodyStyle = computed(() => {
|
||||||
|
if (fullscreen.value) {
|
||||||
|
return {
|
||||||
|
height: '100vh',
|
||||||
|
...props.bodyStyle
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
height: dialogHeight.value,
|
||||||
|
...props.bodyStyle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
dialogOpen,
|
||||||
|
async (to) => {
|
||||||
|
if (to) {
|
||||||
|
await nextTick();
|
||||||
|
dialogRef.value.focus();
|
||||||
|
if (props.blockScroll) {
|
||||||
|
// 锁定滚动
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
document.body.style.overflowY = 'hidden';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (props.blockScroll) {
|
||||||
|
// 解锁滚动
|
||||||
|
document.documentElement.style.removeProperty('overflow-y');
|
||||||
|
document.body.style.removeProperty('overflow-y');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watchEffect(() => {
|
||||||
|
dialogOpen.value = props.open;
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', getClickPosition, true); // 事件在捕获阶段执行
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', getClickPosition, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getClickPosition(e: MouseEvent) {
|
||||||
|
if (!dialogOpen.value) {
|
||||||
|
mousePosition.value = {
|
||||||
|
x: e.clientX, // 相对于浏览器视口左上角的 X 坐标,不页面滚动而改变
|
||||||
|
y: e.clientY // 相对于浏览器视口左上角的 Y 坐标,不页面滚动而改变
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onBeforeEnter(el: Element) {
|
||||||
|
showDialogWrap.value = true;
|
||||||
|
await nextTick();
|
||||||
|
if (props.transformOrigin === 'mouse' && mousePosition.value) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
transformOrigin.value = `${mousePosition.value.x - rect.left}px ${mousePosition.value.y - rect.top}px`;
|
||||||
|
} else {
|
||||||
|
transformOrigin.value = '50% 50%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBeforeLeave(el: Element) {
|
||||||
|
if (props.transformOrigin === 'mouse' && mousePosition.value) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
transformOrigin.value = `${mousePosition.value.x - rect.left}px ${mousePosition.value.y - rect.top}px`;
|
||||||
|
} else {
|
||||||
|
transformOrigin.value = '50% 50%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAfterLeave() {
|
||||||
|
showDialogWrap.value = false;
|
||||||
|
// 重置全屏显示
|
||||||
|
fullscreen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFullScreen() {
|
||||||
|
fullscreen.value = !fullscreen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
dialogOpen.value = false;
|
||||||
|
emits('update:open', false);
|
||||||
|
emits('cancel');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOk() {
|
||||||
|
emits('ok');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-dialog-root">
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-show="dialogOpen" class="m-dialog-mask" :style="maskStyle"></div>
|
||||||
|
</Transition>
|
||||||
|
<div
|
||||||
|
v-show="showDialogWrap"
|
||||||
|
tabindex="-1"
|
||||||
|
ref="dialogRef"
|
||||||
|
class="m-dialog-wrap"
|
||||||
|
:class="{ 'flex-centered': centered }"
|
||||||
|
@click.self="props.maskClosable ? onCancel() : () => false"
|
||||||
|
@keydown.esc="props.keyboard ? onCancel() : () => false"
|
||||||
|
>
|
||||||
|
<Transition
|
||||||
|
name="zoom"
|
||||||
|
enter-from-class="zoom-enter"
|
||||||
|
enter-active-class="zoom-enter"
|
||||||
|
enter-to-class="zoom-enter zoom-enter-active"
|
||||||
|
leave-from-class="zoom-leave"
|
||||||
|
leave-active-class="zoom-leave zoom-leave-active"
|
||||||
|
leave-to-class="zoom-leave zoom-leave-active"
|
||||||
|
@before-enter="onBeforeEnter"
|
||||||
|
@before-leave="onBeforeLeave"
|
||||||
|
@after-leave="onAfterLeave"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="dialogOpen"
|
||||||
|
class="m-dialog"
|
||||||
|
:class="{ 'dialog-with-fullscreen': fullscreen }"
|
||||||
|
:style="dialogStyle"
|
||||||
|
>
|
||||||
|
<div class="m-dialog-body-wrap" :class="bodyClass" :style="dialogBodyStyle">
|
||||||
|
<div class="dialog-header" :class="{ 'header-with-switch': switchFullscreen }" :style="titleStyle">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</div>
|
||||||
|
<span v-if="switchFullscreen" class="fullscreen-action" @click="onFullScreen">
|
||||||
|
<svg
|
||||||
|
v-show="!fullscreen"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="fullscreen"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M290 236.4l43.9-43.9a8.01 8.01 0 00-4.7-13.6L169 160c-5.1-.6-9.5 3.7-8.9 8.9L179 329.1c.8 6.6 8.9 9.4 13.6 4.7l43.7-43.7L370 423.7c3.1 3.1 8.2 3.1 11.3 0l42.4-42.3c3.1-3.1 3.1-8.2 0-11.3L290 236.4zm352.7 187.3c3.1 3.1 8.2 3.1 11.3 0l133.7-133.6 43.7 43.7a8.01 8.01 0 0013.6-4.7L863.9 169c.6-5.1-3.7-9.5-8.9-8.9L694.8 179c-6.6.8-9.4 8.9-4.7 13.6l43.9 43.9L600.3 370a8.03 8.03 0 000 11.3l42.4 42.4zM845 694.9c-.8-6.6-8.9-9.4-13.6-4.7l-43.7 43.7L654 600.3a8.03 8.03 0 00-11.3 0l-42.4 42.3a8.03 8.03 0 000 11.3L734 787.6l-43.9 43.9a8.01 8.01 0 004.7 13.6L855 864c5.1.6 9.5-3.7 8.9-8.9L845 694.9zm-463.7-94.6a8.03 8.03 0 00-11.3 0L236.3 733.9l-43.7-43.7a8.01 8.01 0 00-13.6 4.7L160.1 855c-.6 5.1 3.7 9.5 8.9 8.9L329.2 845c6.6-.8 9.4-8.9 4.7-13.6L290 787.6 423.7 654c3.1-3.1 3.1-8.2 0-11.3l-42.4-42.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-show="fullscreen"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="fullscreen-exit"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M391 240.9c-.8-6.6-8.9-9.4-13.6-4.7l-43.7 43.7L200 146.3a8.03 8.03 0 00-11.3 0l-42.4 42.3a8.03 8.03 0 000 11.3L280 333.6l-43.9 43.9a8.01 8.01 0 004.7 13.6L401 410c5.1.6 9.5-3.7 8.9-8.9L391 240.9zm10.1 373.2L240.8 633c-6.6.8-9.4 8.9-4.7 13.6l43.9 43.9L146.3 824a8.03 8.03 0 000 11.3l42.4 42.3c3.1 3.1 8.2 3.1 11.3 0L333.7 744l43.7 43.7A8.01 8.01 0 00391 783l18.9-160.1c.6-5.1-3.7-9.4-8.8-8.8zm221.8-204.2L783.2 391c6.6-.8 9.4-8.9 4.7-13.6L744 333.6 877.7 200c3.1-3.1 3.1-8.2 0-11.3l-42.4-42.3a8.03 8.03 0 00-11.3 0L690.3 279.9l-43.7-43.7a8.01 8.01 0 00-13.6 4.7L614.1 401c-.6 5.2 3.7 9.5 8.8 8.9zM744 690.4l43.9-43.9a8.01 8.01 0 00-4.7-13.6L623 614c-5.1-.6-9.5 3.7-8.9 8.9L633 783.1c.8 6.6 8.9 9.4 13.6 4.7l43.7-43.7L824 877.7c3.1 3.1 8.2 3.1 11.3 0l42.4-42.3c3.1-3.1 3.1-8.2 0-11.3L744 690.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="close-action" @click="onCancel">
|
||||||
|
<svg
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="close"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<div class="dialog-content" :style="contentStyle">
|
||||||
|
<slot>{{ content }}</slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="footer" class="dialog-footer">
|
||||||
|
<slot name="footer">
|
||||||
|
<Button class="mr8" @click="onCancel" v-bind="cancelProps">{{ cancelText }}</Button>
|
||||||
|
<Button :type="okType" :loading="props.confirmLoading" @click="onOk" v-bind="okProps">{{
|
||||||
|
okText
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-enter {
|
||||||
|
transform: none;
|
||||||
|
opacity: 0;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-enter-active {
|
||||||
|
animation-name: zoomIn;
|
||||||
|
animation-play-state: running;
|
||||||
|
@keyframes zoomIn {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-leave {
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-play-state: paused;
|
||||||
|
animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-leave-active {
|
||||||
|
animation-name: zoomOut;
|
||||||
|
animation-play-state: running;
|
||||||
|
@keyframes zoomOut {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-dialog-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-dialog-wrap {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
overflow: auto;
|
||||||
|
outline: 0;
|
||||||
|
z-index: 1010;
|
||||||
|
|
||||||
|
.m-dialog {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
width: auto;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
padding-bottom: 24px;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.m-dialog-body-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 20px 24px;
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
max-width: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-with-switch {
|
||||||
|
max-width: calc(100% - 54px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-action {
|
||||||
|
.close-action();
|
||||||
|
right: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-action {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 18px;
|
||||||
|
z-index: 1010;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
fill: currentColor;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow: auto;
|
||||||
|
transition: all 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: end;
|
||||||
|
background: transparent;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.mr8 {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-with-fullscreen {
|
||||||
|
max-width: 100%;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-centered {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.m-dialog {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
src/components/MyUI/Divider/Divider.vue
Normal file
166
src/components/MyUI/Divider/Divider.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orientation?: 'left' | 'center' | 'right' // 分割线标题的位置
|
||||||
|
orientationMargin?: string | number // 标题和最近 left/right 边框之间的距离,去除了分割线,同时 orientation 必须为 left 或 right
|
||||||
|
borderWidth?: number // 分割线宽度,单位 px
|
||||||
|
borderStyle?: 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' // 分割线样式
|
||||||
|
borderColor?: string // 分割线颜色
|
||||||
|
vertical?: boolean // 是否垂直分割
|
||||||
|
height?: string | number // 垂直分割线高度,仅当 vertical: true 时生效
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
orientation: 'center',
|
||||||
|
orientationMargin: undefined,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: 'rgba(5, 5, 5, 0.06)',
|
||||||
|
vertical: false,
|
||||||
|
height: '0.9em'
|
||||||
|
});
|
||||||
|
const slotsExist = useSlotsExist(['default']);
|
||||||
|
const margin = computed(() => {
|
||||||
|
if (typeof props.orientationMargin === 'number') {
|
||||||
|
return `${props.orientationMargin}px`;
|
||||||
|
}
|
||||||
|
return props.orientationMargin;
|
||||||
|
});
|
||||||
|
const lineHeight = computed(() => {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
}
|
||||||
|
return props.height;
|
||||||
|
});
|
||||||
|
const showText = computed(() => {
|
||||||
|
return slotsExist.default;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-divider"
|
||||||
|
:class="[
|
||||||
|
vertical ? 'divider-vertical' : 'divider-horizontal',
|
||||||
|
{
|
||||||
|
'divider-with-text': showText,
|
||||||
|
'divider-with-text-center': showText && orientation === 'center',
|
||||||
|
'divider-with-text-left': showText && orientation === 'left',
|
||||||
|
'divider-with-text-right': showText && orientation === 'right',
|
||||||
|
'divider-orientation-margin-left': showText && orientation === 'left' && orientationMargin !== undefined,
|
||||||
|
'divider-orientation-margin-right': showText && orientation === 'right' && orientationMargin !== undefined
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:style="`--border-width: ${borderWidth}px; --border-style: ${borderStyle}; --border-color: ${borderColor}; --margin: ${margin}; --line-height: ${lineHeight};`"
|
||||||
|
>
|
||||||
|
<span v-if="showText" class="divider-text">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-divider {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
border-top: var(--border-width) var(--border-style) var(--border-color);
|
||||||
|
|
||||||
|
.divider-text {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-horizontal {
|
||||||
|
display: flex;
|
||||||
|
clear: both;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-vertical {
|
||||||
|
position: relative;
|
||||||
|
top: -0.06em;
|
||||||
|
display: inline-block;
|
||||||
|
height: var(--line-height);
|
||||||
|
margin: 0 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-top: 0;
|
||||||
|
border-left: var(--border-width) var(--border-style) var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-with-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 0 var(--border-color);
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
position: relative;
|
||||||
|
width: 50%;
|
||||||
|
border-top-width: var(--border-width);
|
||||||
|
border-top-style: var(--border-style);
|
||||||
|
border-top-color: inherit;
|
||||||
|
transform: translateY(50%);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-with-text-left {
|
||||||
|
&::before {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-with-text-right {
|
||||||
|
&::before {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-orientation-margin-left {
|
||||||
|
&::before {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-text {
|
||||||
|
margin-left: var(--margin);
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-orientation-margin-right {
|
||||||
|
&::before {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-text {
|
||||||
|
margin-right: var(--margin);
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
386
src/components/MyUI/Drawer/Drawer.vue
Normal file
386
src/components/MyUI/Drawer/Drawer.vue
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed, ref, watch, watchEffect} from 'vue';
|
||||||
|
import Scrollbar from '../Scrollbar/Scrollbar.vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 抽屉宽度,在 placement 为 right 或 left 时使用,单位 px
|
||||||
|
height?: string | number // 抽屉高度,在 placement 为 top 或 bottom 时使用,单位 px
|
||||||
|
title?: string // 标题 string | slot
|
||||||
|
closable?: boolean // 是否显示左上角的关闭按钮
|
||||||
|
placement?: 'top' | 'right' | 'bottom' | 'left' // 抽屉的方向
|
||||||
|
headerClass?: string // 设置 Drawer 头部的类名
|
||||||
|
headerStyle?: CSSProperties // 设置 Drawer 头部的样式
|
||||||
|
scrollbarProps?: object // Scrollbar 组件属性配置,用于设置内容滚动条的样式
|
||||||
|
bodyClass?: string // 设置 Drawer 内容部分的类名
|
||||||
|
bodyStyle?: CSSProperties // 设置 Drawer 内容部分的样式
|
||||||
|
extra?: string // 抽屉右上角的操作区域 string | slot
|
||||||
|
footer?: string // 抽屉的页脚 string | slot
|
||||||
|
footerClass?: string // 设置 Drawer 页脚的类名
|
||||||
|
footerStyle?: CSSProperties // 设置 Drawer 页脚的样式
|
||||||
|
destroyOnClose?: boolean // 关闭时是否销毁 Drawer 里的子元素
|
||||||
|
zIndex?: number // 设置 Drawer 的 z-index
|
||||||
|
open?: boolean // (v-model) 抽屉是否可见
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 378,
|
||||||
|
height: 378,
|
||||||
|
title: undefined,
|
||||||
|
closable: true,
|
||||||
|
placement: 'right',
|
||||||
|
headerClass: undefined,
|
||||||
|
headerStyle: () => ({}),
|
||||||
|
scrollbarProps: () => ({}),
|
||||||
|
bodyClass: undefined,
|
||||||
|
bodyStyle: () => ({}),
|
||||||
|
extra: undefined,
|
||||||
|
footer: undefined,
|
||||||
|
footerClass: undefined,
|
||||||
|
footerStyle: () => ({}),
|
||||||
|
destroyOnClose: false,
|
||||||
|
zIndex: 1000,
|
||||||
|
open: false
|
||||||
|
});
|
||||||
|
const drawerRef = ref();
|
||||||
|
const drawerOpen = ref<boolean>();
|
||||||
|
const slotsExist = useSlotsExist(['title', 'extra', 'footer']);
|
||||||
|
const emits = defineEmits(['update:open', 'close']);
|
||||||
|
const drawerWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const drawerHeight = computed(() => {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
}
|
||||||
|
return props.height;
|
||||||
|
});
|
||||||
|
const drawerStyle = computed(() => {
|
||||||
|
if (['top', 'bottom'].includes(props.placement)) {
|
||||||
|
return {
|
||||||
|
zIndex: props.zIndex,
|
||||||
|
height: drawerHeight.value
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
zIndex: props.zIndex,
|
||||||
|
width: drawerWidth.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const showHeader = computed(() => {
|
||||||
|
return slotsExist.title || slotsExist.extra || props.title || props.extra || props.closable;
|
||||||
|
});
|
||||||
|
const showTitle = computed(() => {
|
||||||
|
return slotsExist.title || props.title;
|
||||||
|
});
|
||||||
|
const showExtra = computed(() => {
|
||||||
|
return slotsExist.extra || props.extra;
|
||||||
|
});
|
||||||
|
const showFooter = computed(() => {
|
||||||
|
return slotsExist.footer || props.footer;
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
drawerOpen,
|
||||||
|
(to) => {
|
||||||
|
if (to) {
|
||||||
|
drawerRef.value.focus();
|
||||||
|
// 锁定滚动
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
document.body.style.overflowY = 'hidden';
|
||||||
|
} else {
|
||||||
|
// 解锁滚动
|
||||||
|
document.documentElement.style.removeProperty('overflow-y');
|
||||||
|
document.body.style.removeProperty('overflow-y');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watchEffect(() => {
|
||||||
|
drawerOpen.value = props.open;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onBlur(e: Event) {
|
||||||
|
drawerOpen.value = false;
|
||||||
|
emits('update:open', false);
|
||||||
|
emits('close', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose(e: Event) {
|
||||||
|
drawerOpen.value = false;
|
||||||
|
emits('update:open', false);
|
||||||
|
emits('close', e);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div ref="drawerRef" tabindex="-1" class="m-drawer" @keydown.esc="onClose">
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-show="drawerOpen" class="m-drawer-mask" @click.self="onBlur"></div>
|
||||||
|
</Transition>
|
||||||
|
<Transition :name="`motion-${placement}`">
|
||||||
|
<div v-show="drawerOpen" class="m-drawer-wrap" :class="`drawer-${placement}`" :style="drawerStyle">
|
||||||
|
<div class="m-drawer-content">
|
||||||
|
<div v-if="!destroyOnClose" class="m-drawer-body-wrapper">
|
||||||
|
<div v-show="showHeader" class="m-drawer-header" :class="headerClass" :style="headerStyle">
|
||||||
|
<div class="m-header-title">
|
||||||
|
<svg
|
||||||
|
v-if="closable"
|
||||||
|
focusable="false"
|
||||||
|
class="svg-close"
|
||||||
|
data-icon="close"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
@click="onClose"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div v-if="showTitle" class="header-title">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showExtra" class="header-extra">
|
||||||
|
<slot name="extra">{{ extra }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Scrollbar v-bind="scrollbarProps">
|
||||||
|
<div class="m-drawer-body" :class="bodyClass" :style="bodyStyle">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</Scrollbar>
|
||||||
|
<div v-if="showFooter" class="m-drawer-footer" :class="footerClass" :style="footerStyle">
|
||||||
|
<slot name="footer">{{ footer }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="destroyOnClose && drawerOpen" class="m-drawer-body-wrapper">
|
||||||
|
<div v-show="showHeader" class="m-drawer-header" :class="headerClass" :style="headerStyle">
|
||||||
|
<div class="m-header-title">
|
||||||
|
<svg
|
||||||
|
v-if="closable"
|
||||||
|
focusable="false"
|
||||||
|
class="svg-close"
|
||||||
|
data-icon="close"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
@click="onClose"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div v-if="showTitle" class="header-title">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showExtra" class="header-extra">
|
||||||
|
<slot name="extra">{{ extra }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Scrollbar v-bind="scrollbarProps">
|
||||||
|
<div class="m-drawer-body" :class="bodyClass" :style="bodyStyle">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</Scrollbar>
|
||||||
|
<div v-if="showFooter" class="m-drawer-footer" :class="footerClass" :style="footerStyle">
|
||||||
|
<slot name="footer">{{ footer }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-top-enter-active,
|
||||||
|
.motion-top-leave-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-top-enter-from,
|
||||||
|
.motion-top-leave-to {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-right-enter-active,
|
||||||
|
.motion-right-leave-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-right-enter-from,
|
||||||
|
.motion-right-leave-to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-bottom-enter-active,
|
||||||
|
.motion-bottom-leave-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-bottom-enter-from,
|
||||||
|
.motion-bottom-leave-to {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-left-enter-active,
|
||||||
|
.motion-left-leave-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-left-enter-from,
|
||||||
|
.motion-left-leave-to {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-drawer {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.m-drawer-mask {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-drawer-wrap {
|
||||||
|
position: absolute;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
.m-drawer-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.m-drawer-body-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.m-drawer-header {
|
||||||
|
display: flex;
|
||||||
|
flex: 0;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
|
||||||
|
.m-header-title {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.svg-close {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
fill: currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-extra {
|
||||||
|
flex: none;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-drawer-body {
|
||||||
|
height: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-drawer-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-top: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-top {
|
||||||
|
top: 0;
|
||||||
|
inset-inline: 0;
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-right {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: -6px 0 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
-3px 0 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
-9px 0 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
inset-inline: 0;
|
||||||
|
box-shadow: 0 -6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 -3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 -9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-left {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
box-shadow: 6px 0 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
3px 0 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
9px 0 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted } from 'vue';
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
import { useResizeObserver } from '../utils';
|
import { useResizeObserver } from '../Utils';
|
||||||
interface Props {
|
interface Props {
|
||||||
maxWidth?: string | number // 文本最大宽度,单位 px
|
maxWidth?: string | number // 文本最大宽度,单位 px
|
||||||
tooltipMaxWidth?: string | number // 弹出提示最大宽度,单位 px,默认为 maxWidth + 24
|
tooltipMaxWidth?: string | number // 弹出提示最大宽度,单位 px,默认为 maxWidth + 24
|
||||||
|
|||||||
140
src/components/MyUI/Empty/Empty.vue
Normal file
140
src/components/MyUI/Empty/Empty.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import { useSlotsExist } from '../Utils';
|
||||||
|
interface Props {
|
||||||
|
description?: string // 自定义描述内容 string | slot
|
||||||
|
descriptionStyle?: CSSProperties // 设置描述文本的样式
|
||||||
|
image?: 'filled' | 'outlined' | string // 显示图片的链接,或者 选择两种预置风格图片 string | slot
|
||||||
|
imageStyle?: CSSProperties // 设置图片的样式
|
||||||
|
footer?: string // 设置底部内容 string | slot
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
description: '暂无数据',
|
||||||
|
descriptionStyle: () => ({}),
|
||||||
|
image: 'filled',
|
||||||
|
imageStyle: () => ({}),
|
||||||
|
footer: undefined
|
||||||
|
});
|
||||||
|
const slotsExist = useSlotsExist(['default', 'description', 'footer']);
|
||||||
|
const showDescription = computed(() => {
|
||||||
|
return slotsExist.description || props.description;
|
||||||
|
});
|
||||||
|
const showFooter = computed(() => {
|
||||||
|
return slotsExist.footer || props.footer;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-empty" :class="{ 'empty-image-outlined': image === 'outlined' }">
|
||||||
|
<div class="m-empty-image" :style="imageStyle">
|
||||||
|
<slot v-if="slotsExist.default"></slot>
|
||||||
|
<svg
|
||||||
|
v-else-if="image === 'filled'"
|
||||||
|
class="empty-filled"
|
||||||
|
:style="imageStyle"
|
||||||
|
viewBox="0 0 184 152"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g transform="translate(24 31.67)">
|
||||||
|
<ellipse fill-opacity=".8" fill="#F5F5F7" cx="67.797" cy="106.89" rx="67.797" ry="12.668"></ellipse>
|
||||||
|
<path
|
||||||
|
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
|
||||||
|
fill="#AEB8C2"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
|
||||||
|
fill="url(#linearGradient-1)"
|
||||||
|
transform="translate(13.56)"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
|
||||||
|
fill="#F5F5F7"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
|
||||||
|
fill="#DCE0E6"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
|
||||||
|
fill="#DCE0E6"
|
||||||
|
></path>
|
||||||
|
<g transform="translate(149.65 15.383)" fill="#FFF">
|
||||||
|
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815"></ellipse>
|
||||||
|
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="image === 'outlined'"
|
||||||
|
class="empty-outlined"
|
||||||
|
:style="imageStyle"
|
||||||
|
viewBox="0 0 64 41"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g transform="translate(0 1)" fill="none" fill-rule="evenodd">
|
||||||
|
<ellipse fill="#f5f5f5" cx="32" cy="33" rx="32" ry="7"></ellipse>
|
||||||
|
<g fill-rule="nonzero" stroke="#d9d9d9">
|
||||||
|
<path
|
||||||
|
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
|
||||||
|
fill="#fafafa"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<img v-else-if="image" class="empty-image" :src="image" alt="empty" />
|
||||||
|
</div>
|
||||||
|
<p v-if="showDescription" class="empty-description" :style="descriptionStyle">
|
||||||
|
<slot name="description">{{ description }}</slot>
|
||||||
|
</p>
|
||||||
|
<div v-if="showFooter" class="empty-footer">
|
||||||
|
<slot name="footer">{{ footer }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-empty {
|
||||||
|
margin-inline: 8px;
|
||||||
|
text-align: center;
|
||||||
|
.m-empty-image {
|
||||||
|
height: 100px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
opacity: 1;
|
||||||
|
.empty-filled {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
width: 184px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
.empty-outlined {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
width: 64px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.empty-image {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
}
|
||||||
|
.empty-footer {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-image-outlined {
|
||||||
|
margin-block: 32px;
|
||||||
|
.m-empty-image {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
75
src/components/MyUI/Flex/Flex.vue
Normal file
75
src/components/MyUI/Flex/Flex.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 弹性区域总宽度,单位 px
|
||||||
|
vertical?: boolean // flex 主轴的方向是否垂直,vertical 使用 flex-direction: column
|
||||||
|
wrap?: 'nowrap' | 'wrap' | 'wrap-reverse' // 设置元素单行显示还是多行显示;参考 flex-wrap
|
||||||
|
justify?: string // 设置元素在主轴方向上的对齐方式;参考 justify-content
|
||||||
|
align?: string // 设置元素在交叉轴方向上的对齐方式;参考 align-items
|
||||||
|
gap?: number | number[] | 'small' | 'middle' | 'large' // 设置网格之间的间隙,数组时表示: [水平间距, 垂直间距]
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 'auto',
|
||||||
|
vertical: false,
|
||||||
|
wrap: 'nowrap',
|
||||||
|
justify: 'normal',
|
||||||
|
align: 'normal',
|
||||||
|
gap: 'middle'
|
||||||
|
});
|
||||||
|
const flexWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const gapValue = computed(() => {
|
||||||
|
if (props.gap === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (typeof props.gap === 'number') {
|
||||||
|
return `${props.gap}px`;
|
||||||
|
}
|
||||||
|
if (Array.isArray(props.gap)) {
|
||||||
|
return `${props.gap[1]}px ${props.gap[0]}px`;
|
||||||
|
}
|
||||||
|
if (['small', 'middle', 'large'].includes(props.gap)) {
|
||||||
|
const gapMap = {
|
||||||
|
small: '8px',
|
||||||
|
middle: '16px',
|
||||||
|
large: '24px'
|
||||||
|
};
|
||||||
|
return gapMap[props.gap];
|
||||||
|
}
|
||||||
|
return '0';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-flex"
|
||||||
|
:class="{ 'flex-vertical': vertical }"
|
||||||
|
:style="`
|
||||||
|
width: ${flexWidth};
|
||||||
|
gap: ${gapValue};
|
||||||
|
margin-bottom: -${Array.isArray(props.gap) && wrap ? props.gap[1] : 0}px;
|
||||||
|
--wrap: ${wrap};
|
||||||
|
--justify: ${justify};
|
||||||
|
--align: ${align};
|
||||||
|
`"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-flex {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: var(--wrap);
|
||||||
|
justify-content: var(--justify);
|
||||||
|
align-items: var(--align);
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.flex-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
398
src/components/MyUI/FloatButton/FloatButton.vue
Normal file
398
src/components/MyUI/FloatButton/FloatButton.vue
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, watch} from 'vue';
|
||||||
|
import Tooltip from '../Tooltip/Tooltip.vue';
|
||||||
|
import Badge from '../Badge/Badge.vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
top?: number | string // 按钮定位的上边距,单位 px
|
||||||
|
bottom?: number | string // 按钮定位的下边距,单位 px
|
||||||
|
left?: number | string // 按钮定位的左边距,单位 px
|
||||||
|
right?: number | string // 按钮定位的右边距,单位 px
|
||||||
|
width?: number | string // 浮动按钮宽度,单位 px
|
||||||
|
height?: number | string // 浮动按钮高度,单位 px
|
||||||
|
type?: 'default' | 'primary' // 浮动按钮类型
|
||||||
|
shape?: 'circle' | 'square' // 浮动按钮形状
|
||||||
|
icon?: string // 浮动按钮图标 string | slot
|
||||||
|
description?: string // 文字描述信息 string | slot
|
||||||
|
href?: string // 点击跳转的地址,指定此属性按钮的行为和 a 链接一致
|
||||||
|
target?: '_self' | '_blank' // 相当于 a 标签的 target 属性,href 存在时生效
|
||||||
|
menuTrigger?: 'click' | 'hover' // 浮动按钮菜单显示的触发方式
|
||||||
|
tooltip?: string // 气泡卡片的内容 string | slot
|
||||||
|
tooltipProps?: object // Tooltip 组件属性配置,参考 Tooltip Props
|
||||||
|
badgeProps?: object // 带徽标的浮动按钮(不支持 status 以及相关属性),参考 Badge Props
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
top: undefined,
|
||||||
|
bottom: 40,
|
||||||
|
left: undefined,
|
||||||
|
right: 40,
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
type: 'default',
|
||||||
|
shape: 'circle',
|
||||||
|
icon: undefined,
|
||||||
|
description: undefined,
|
||||||
|
href: undefined,
|
||||||
|
target: '_self',
|
||||||
|
menuTrigger: undefined,
|
||||||
|
tooltip: undefined,
|
||||||
|
tooltipProps: () => ({}),
|
||||||
|
badgeProps: () => ({})
|
||||||
|
});
|
||||||
|
const showMenu = ref(false);
|
||||||
|
const emits = defineEmits(['click', 'openChange']);
|
||||||
|
const slotsExist = useSlotsExist(['icon', 'description', 'tooltip', 'menu']);
|
||||||
|
const floatBtnWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const floatBtnHeight = computed(() => {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
}
|
||||||
|
return props.height;
|
||||||
|
});
|
||||||
|
const floatBtnLeft = computed(() => {
|
||||||
|
if (typeof props.left === 'number') {
|
||||||
|
return `${props.left}px`;
|
||||||
|
}
|
||||||
|
return props.left;
|
||||||
|
});
|
||||||
|
const floatBtnRight = computed(() => {
|
||||||
|
if (props.left) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
if (typeof props.right === 'number') {
|
||||||
|
return `${props.right}px`;
|
||||||
|
}
|
||||||
|
return props.right;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const floatBtnTop = computed(() => {
|
||||||
|
if (typeof props.top === 'number') {
|
||||||
|
return `${props.top}px`;
|
||||||
|
}
|
||||||
|
return props.top;
|
||||||
|
});
|
||||||
|
const floatBtnBottom = computed(() => {
|
||||||
|
if (props.top) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
if (typeof props.bottom === 'number') {
|
||||||
|
return `${props.bottom}px`;
|
||||||
|
}
|
||||||
|
return props.bottom;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const showDescription = computed(() => {
|
||||||
|
return slotsExist.description || props.description;
|
||||||
|
});
|
||||||
|
const showTooltip = computed(() => {
|
||||||
|
return slotsExist.tooltip || props.tooltip;
|
||||||
|
});
|
||||||
|
watch(showMenu, (to) => {
|
||||||
|
emits('openChange', to);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onClick(e: Event) {
|
||||||
|
emits('click', e);
|
||||||
|
if (props.menuTrigger === 'click' && slotsExist.menu) {
|
||||||
|
showMenu.value = !showMenu.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="href ? 'a' : 'div'"
|
||||||
|
tabindex="0"
|
||||||
|
class="m-float-btn"
|
||||||
|
:class="`float-btn-${type} float-btn-${shape}`"
|
||||||
|
:style="`
|
||||||
|
--float-btn-width: ${floatBtnWidth};
|
||||||
|
--float-btn-height: ${floatBtnHeight};
|
||||||
|
--float-btn-left: ${floatBtnLeft};
|
||||||
|
--float-btn-right: ${floatBtnRight};
|
||||||
|
--float-btn-top: ${floatBtnTop};
|
||||||
|
--float-btn-bottom: ${floatBtnBottom}
|
||||||
|
`"
|
||||||
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
@click="onClick"
|
||||||
|
@blur="menuTrigger === 'click' ? (showMenu = false) : null"
|
||||||
|
@mouseenter="menuTrigger === 'hover' ? (showMenu = true) : null"
|
||||||
|
@mouseleave="menuTrigger === 'hover' ? (showMenu = false) : null"
|
||||||
|
>
|
||||||
|
<Tooltip placement="left" v-bind="tooltipProps" class="float-btn-tooltip">
|
||||||
|
<template v-if="showTooltip" #tooltip>
|
||||||
|
<slot name="tooltip">{{ tooltip }}</slot>
|
||||||
|
</template>
|
||||||
|
<Badge v-bind="badgeProps">
|
||||||
|
<div class="float-btn-body">
|
||||||
|
<div class="float-btn-content">
|
||||||
|
<div v-if="slotsExist.icon" class="float-btn-icon">
|
||||||
|
<Transition name="fade">
|
||||||
|
<slot v-if="!showMenu" name="icon"></slot>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="close-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
<div v-if="showDescription" class="float-btn-description">
|
||||||
|
<slot name="description">{{ description }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
<Transition v-show="showMenu" name="move">
|
||||||
|
<div class="float-btn-menu">
|
||||||
|
<slot name="menu"></slot>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.fade-move,
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
transform: scale(0.75);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-enter-active,
|
||||||
|
.move-leave-active {
|
||||||
|
transform-origin: 0 0;
|
||||||
|
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-leave-active {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-enter-from,
|
||||||
|
.move-leave-to {
|
||||||
|
transform: translate3d(0, var(--float-btn-height), 0);
|
||||||
|
transform-origin: 0 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-float-btn {
|
||||||
|
position: fixed;
|
||||||
|
left: var(--float-btn-left);
|
||||||
|
right: var(--float-btn-right);
|
||||||
|
top: var(--float-btn-top);
|
||||||
|
bottom: var(--float-btn-bottom);
|
||||||
|
z-index: 99;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--float-btn-width);
|
||||||
|
height: var(--float-btn-height);
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.float-btn-tooltip {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
:deep(.tooltip-content) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.m-badge {
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.m-badge-value:not(.only-dot) {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
transform-origin: center;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-body {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
.float-btn-content {
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
min-height: var(--float-btn-height);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 4px;
|
||||||
|
|
||||||
|
.float-btn-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
.close-svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(img) {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-menu {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
display: block;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
.m-float-btn {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-default {
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
& > .float-btn-tooltip {
|
||||||
|
.float-btn-body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-content {
|
||||||
|
.float-btn-icon {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-primary {
|
||||||
|
background-color: @themeColor;
|
||||||
|
|
||||||
|
& > .float-btn-tooltip {
|
||||||
|
.float-btn-body {
|
||||||
|
background-color: @themeColor;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #4096ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-content {
|
||||||
|
.float-btn-icon {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
:deep(.m-badge) {
|
||||||
|
.only-dot {
|
||||||
|
top: 5.857864376269049px;
|
||||||
|
right: 5.857864376269049px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .float-btn-tooltip {
|
||||||
|
.float-btn-body {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-square {
|
||||||
|
height: auto;
|
||||||
|
min-height: var(--float-btn-height);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
:deep(.m-badge) {
|
||||||
|
.only-dot {
|
||||||
|
top: 2.3431457505076194px;
|
||||||
|
right: 2.3431457505076194px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .float-btn-tooltip {
|
||||||
|
.float-btn-body {
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
408
src/components/MyUI/GaugeChart/GaugeChart.vue
Normal file
408
src/components/MyUI/GaugeChart/GaugeChart.vue
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, onMounted, ref, watch} from 'vue';
|
||||||
|
/*
|
||||||
|
按需引入
|
||||||
|
*/
|
||||||
|
// 使用 ECharts 提供的按需引入的接口来打包必须的组件
|
||||||
|
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
// 引入仪表盘图表,图表后缀都为 Chart
|
||||||
|
import {GaugeChart} from 'echarts/charts';
|
||||||
|
// 引入提示框,组件后缀都为 Component
|
||||||
|
import {TooltipComponent} from 'echarts/components';
|
||||||
|
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
|
||||||
|
import {CanvasRenderer} from 'echarts/renderers';
|
||||||
|
// 注册必须的组件
|
||||||
|
echarts.use([GaugeChart, TooltipComponent, CanvasRenderer]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
全部引入
|
||||||
|
*/
|
||||||
|
// import * as echarts from 'echarts'
|
||||||
|
/*
|
||||||
|
需要注意的是为了保证打包的体积是最小的,ECharts 按需引入的时候不再提供任何渲染器,
|
||||||
|
所以需要选择引入 CanvasRenderer 或者 SVGRenderer 作为渲染器。这样的好处是假如
|
||||||
|
你只需要使用 svg 渲染模式,打包的结果中就不会再包含无需使用的 CanvasRenderer 模块
|
||||||
|
*/
|
||||||
|
|
||||||
|
const chart = ref();
|
||||||
|
const gaugeChart = ref();
|
||||||
|
const gradient = ref({ // 自定义渐变色
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0, color: '#FF6E76' // 0% 处的颜色
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 0.25, color: '#FDDD60' // 25% 处的颜色
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 0.75, color: '#58D9F9' // 75% 处的颜色
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1, color: '#7CFFB2' // 100% 处的颜色
|
||||||
|
}
|
||||||
|
],
|
||||||
|
global: false // 缺省为 false
|
||||||
|
});
|
||||||
|
var option: any;
|
||||||
|
|
||||||
|
interface Gauge {
|
||||||
|
name: string // 数据项名称
|
||||||
|
value: number // 数据值
|
||||||
|
[propName: string]: any // 添加一个字符串索引签名,用于包含带有任意数量的其他属性
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gaugeData: Gauge[] // 仪表盘数据源
|
||||||
|
width?: string | number // 容器宽度
|
||||||
|
height?: string | number // 容器高度
|
||||||
|
themeColor?: string // 主题色
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
gaugeData: () => [],
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
themeColor: '#1677FF'
|
||||||
|
});
|
||||||
|
const chartWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return props.width + 'px';
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const chartHeight = computed(() => {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return props.height + 'px';
|
||||||
|
} else {
|
||||||
|
return props.height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
initChart(); // 初始化图标示例
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.gaugeData,
|
||||||
|
(to) => {
|
||||||
|
// 监听并更新图例数据
|
||||||
|
option.series[0].data = to;
|
||||||
|
gaugeChart.value.setOption(option);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => [props.width, props.height, props.themeColor],
|
||||||
|
() => {
|
||||||
|
if (gaugeChart.value) {
|
||||||
|
gaugeChart.value.dispose(); // 销毁实例
|
||||||
|
}
|
||||||
|
initChart(); // 重新初始化实例
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
flush: 'post'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// const loadingConfig = {
|
||||||
|
// text: 'loading',
|
||||||
|
// color: '#c23531',
|
||||||
|
// textColor: '#000',
|
||||||
|
// maskColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
// zlevel: 0,
|
||||||
|
// 字体大小。从 `v4.8.0` 开始支持。
|
||||||
|
// fontSize: 12,
|
||||||
|
// 是否显示旋转动画(spinner)。从 `v4.8.0` 开始支持。
|
||||||
|
// showSpinner: true,
|
||||||
|
// 旋转动画(spinner)的半径。从 `v4.8.0` 开始支持。
|
||||||
|
// spinnerRadius: 20,
|
||||||
|
// 旋转动画(spinner)的线宽。从 `v4.8.0` 开始支持。
|
||||||
|
// lineWidth: 5,
|
||||||
|
// 字体粗细。从 `v5.0.1` 开始支持。
|
||||||
|
// fontWeight: 'normal',
|
||||||
|
// 字体风格。从 `v5.0.1` 开始支持。
|
||||||
|
// fontStyle: 'normal',
|
||||||
|
// 字体系列。从 `v5.0.1` 开始支持。
|
||||||
|
// fontFamily: 'sans-serif'
|
||||||
|
// }
|
||||||
|
function showLoading(config: any) {
|
||||||
|
gaugeChart.value.showLoading('default', {text: '', color: props.themeColor, ...config}); // 显示加载动画效果
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading() {
|
||||||
|
gaugeChart.value.hideLoading(); // 隐藏动画加载效果
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
showLoading,
|
||||||
|
hideLoading
|
||||||
|
});
|
||||||
|
|
||||||
|
function initChart() {
|
||||||
|
// 等价于使用 Canvas 渲染器(默认):echarts.init(containerDom, null, { renderer: 'canvas' })
|
||||||
|
gaugeChart.value = echarts.init(chart.value);
|
||||||
|
option = {
|
||||||
|
tooltip: { // 提示框浮层设置
|
||||||
|
trigger: 'item',
|
||||||
|
triggerOn: 'mousemove', // 提示框触发条件
|
||||||
|
enterable: true, // 鼠标是否可进入提示框浮层中,默认false
|
||||||
|
confine: true, // 是否将tooltip框限制在图表的区域内
|
||||||
|
formatter: function (params: any) { // 提示框浮层内容格式器,支持字符串模板和回调函数两种形式
|
||||||
|
// console.log('params:', params)
|
||||||
|
return params.marker + params.name + ': ' + params.value || '--';
|
||||||
|
},
|
||||||
|
backgroundColor: 'transparent', // 提示框浮层的背景颜色
|
||||||
|
borderColor: '#7CFFB2', // 提示框浮层的边框颜色
|
||||||
|
borderWidth: 1, // 提示框浮层的边框宽
|
||||||
|
borderRadius: 6, // 提示框浮层圆角
|
||||||
|
padding: [6, 12], // 提示框浮层的内边距
|
||||||
|
textStyle: { // 提示框浮层的文本样式
|
||||||
|
color: '#333', // 文字颜色
|
||||||
|
fontWeight: 600, // 字体粗细
|
||||||
|
fontSize: 18, // 字体大小
|
||||||
|
// lineHeight: 24, // 行高
|
||||||
|
// width: 60, // 文本显示宽度
|
||||||
|
// 文字超出宽度是否截断或者换行;只有配置width时有效
|
||||||
|
overflow: 'breakAll', // truncate截断,并在末尾显示ellipsis配置的文本,默认为...;break换行;breakAll换行,并强制单词内换行
|
||||||
|
ellipsis: '...'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: [props.themeColor],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'gauge',
|
||||||
|
name: '仪表盘', // 系列名称,用于tooltip的显示
|
||||||
|
/*
|
||||||
|
从调色盘 option.color 中取色的策略,可选 'series' | 'data'
|
||||||
|
'series': 按照系列分配调色盘中的颜色,同一系列中的所有数据都是用相同的颜色
|
||||||
|
'data': 按照数据项分配调色盘中的颜色,每个数据项都使用不同的颜色
|
||||||
|
*/
|
||||||
|
colorBy: 'data',
|
||||||
|
center: ['50%', '65%'], // 圆心坐标,[400, 300]: 数组的第一项是横坐标,第二项是纵坐标,支持设置成百分比,['50%', '75%']: 设置成百分比时第一项是相对于容器宽度,第二项是相对于容器高度
|
||||||
|
radius: '100%', // 仪表盘半径,可以是相对于容器高宽中较小的一项的一半的百分比,也可以是绝对的数值。
|
||||||
|
legendHoverLink: true, // 是否启用图例 hover 时的联动高亮
|
||||||
|
startAngle: 210, // 仪表盘起始角度。圆心 正右手侧为0度,正上方为90度,正左手侧为180度。
|
||||||
|
endAngle: -30, // 仪表盘结束角度。
|
||||||
|
clockwise: true, // 仪表盘刻度是否是顺时针增长
|
||||||
|
min: 0, // 最小的数据值,映射到 minAngle
|
||||||
|
max: 100, // 最大的数据值,映射到 maxAngle
|
||||||
|
splitNumber: 10, // 仪表盘刻度的分割段数
|
||||||
|
axisLine: { // 仪表盘轴线相关配置
|
||||||
|
show: true, // 是否显示仪表盘轴线
|
||||||
|
roundCap: true, // 是否在两端显示成圆形
|
||||||
|
lineStyle: { // 仪表盘轴线样式
|
||||||
|
width: 30, // 轴线宽度
|
||||||
|
// color: [ // 仪表盘的轴线可以被分成不同颜色的多段。每段的结束位置和颜色可以通过一个数组来表示
|
||||||
|
// [20, '#FF6E76'],
|
||||||
|
// [40, '#FDDD60'],
|
||||||
|
// [60, '#58D9F9'],
|
||||||
|
// [80, '#7CFFB2'],
|
||||||
|
// [100, '#1677FF']
|
||||||
|
// ],
|
||||||
|
// shadowBlur: 10, // 图形阴影的模糊大小
|
||||||
|
// shadowColor: 'rgba(0, 0, 0, 0.5)', // 阴影颜色
|
||||||
|
// shadowOffsetX: 3, // 阴影水平方向上的偏移距离
|
||||||
|
// shadowOffsetY: 3, // 阴影垂直方向上的偏移距离
|
||||||
|
// opacity: 1 // 图形透明度。支持从 0 到 1 的数字,为 0 时不绘制该图形。
|
||||||
|
}
|
||||||
|
},
|
||||||
|
progress: { // 展示当前进度
|
||||||
|
show: true, // 是否显示进度条
|
||||||
|
overlap: false, // 多组数据时进度条是否重叠
|
||||||
|
// width: 12, // 进度条宽度
|
||||||
|
roundCap: true, // 是否在两端显示成圆形
|
||||||
|
clip: true, // 是否裁掉超出部分
|
||||||
|
itemStyle: { // 进度条样式
|
||||||
|
color: gradient.value, // 图形的颜色
|
||||||
|
// borderColor: '#1677FF', // 图形的描边颜色
|
||||||
|
// borderWidth: 1, // 描边线宽。为 0 时无描边。
|
||||||
|
// borderType: 'solid', // 描边类型,可选:'solid' 'dashed' 'dotted'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: { // 分隔线样式
|
||||||
|
show: true, // 是否显示分隔线
|
||||||
|
length: 30, // 分隔线线长。支持相对半径的百分比
|
||||||
|
distance: 10, // 分隔线与轴线的距离
|
||||||
|
lineStyle: { // 分隔线样式
|
||||||
|
color: 'auto', // 线的颜色
|
||||||
|
width: 5, // 线宽
|
||||||
|
type: 'solid', // 线的类型,可选 'solid' 'dashed' 'dotted'
|
||||||
|
cap: 'butt' // 指定线段末端的绘制方式,可选 'butt'(默认) 线段末端以方形结束 'round' 线段末端以圆形结束 'square' 线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisTick: { // 刻度样式
|
||||||
|
show: true, // 是否显示刻度
|
||||||
|
splitNumber: 10, // 分隔线之间分割的刻度数
|
||||||
|
length: 12, // 刻度线长。支持相对半径的百分比
|
||||||
|
distance: 10, // 刻度线与轴线的距离
|
||||||
|
lineStyle: { // 刻度线样式
|
||||||
|
color: 'auto', // 线的颜色
|
||||||
|
width: 2, // 线宽
|
||||||
|
type: 'solid', // 线的类型,可选 'solid' 'dashed' 'dotted'
|
||||||
|
cap: 'butt' // 指定线段末端的绘制方式,可选 'butt'(默认) 线段末端以方形结束 'round' 线段末端以圆形结束 'square' 线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: { // 刻度标签
|
||||||
|
show: true, // 是否显示标签
|
||||||
|
distance: -80, // 标签与刻度线的距离
|
||||||
|
/*
|
||||||
|
如果是 number 类型,则表示标签的旋转角,从 -90 度到 90 度,正值是逆时针。
|
||||||
|
除此之外,还可以是字符串 'radial' 表示径向旋转、'tangential' 表示切向旋转。
|
||||||
|
如果不需要文字旋转,可以将其设为 0。
|
||||||
|
*/
|
||||||
|
rotate: 'tangential',
|
||||||
|
// 刻度标签的内容格式器,支持字符串模板和回调函数两种形式
|
||||||
|
// formatter: '{value} kg', // // 使用字符串模板,模板变量为刻度默认标签 {value}
|
||||||
|
formatter: function (value: number) {// 使用函数模板,函数参数分别为刻度数值
|
||||||
|
// console.log('value', value)
|
||||||
|
if (value === 90) {
|
||||||
|
return 'A';
|
||||||
|
} else if (value === 70) {
|
||||||
|
return 'B';
|
||||||
|
} else if (value === 50) {
|
||||||
|
return 'C';
|
||||||
|
} else if (value === 30) {
|
||||||
|
return 'D';
|
||||||
|
} else if (value === 10) {
|
||||||
|
return 'F';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
color: '#aaa', // 文字的颜色
|
||||||
|
fontStyle: 'normal', // 文字字体的风格,可选 'normal' 'italic' 'oblique'
|
||||||
|
fontWeight: 'bold', // 文字字体的粗细,可选 'normal' 'bold' 'bolder' 'lighter' 100 | 200 | 300 | 400...
|
||||||
|
fontFamily: 'sans-serif', // 文字的字体系列,还可以是 'serif' , 'monospace', 'Arial', 'Courier New', 'Microsoft YaHei', ...
|
||||||
|
fontSize: 40 // 文字的字体大小
|
||||||
|
// lineHeight: 28, // 行高
|
||||||
|
/*
|
||||||
|
可以使用颜色值,例如:'#123234', 'red', 'rgba(0,23,11,0.3)'
|
||||||
|
也可以直接使用图片,例如:
|
||||||
|
backgroundColor: {
|
||||||
|
image: 'xxx/xxx.png'
|
||||||
|
// 这里可以是图片的 URL,
|
||||||
|
// 或者图片的 dataURI,
|
||||||
|
// 或者 HTMLImageElement 对象,
|
||||||
|
// 或者 HTMLCanvasElement 对象。
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// backgroundColor: 'transparent', // 文字块背景色
|
||||||
|
// borderColor: 'red', // 文字块边框颜色
|
||||||
|
// borderWidth: 3, // 文字块边框宽度
|
||||||
|
// borderType: 'solid', // 文字块边框描边类型,可选 'solid' 'dashed' 'dotted'
|
||||||
|
// borderRadius: 10, // 文字块的圆角
|
||||||
|
// padding: [6, 12], // 文字块的内边距,文字块宽高不包含 padding
|
||||||
|
// width: 60, // 文本显示宽度
|
||||||
|
// height: 60 // 文本显示高度
|
||||||
|
},
|
||||||
|
pointer: { // 仪表盘指针
|
||||||
|
show: true, // 是否显示指针
|
||||||
|
showAbove: true, // 指针是否显示在标题和仪表盘详情上方
|
||||||
|
// 可以通过 'image://url' 设置为图片,其中 URL 为图片的链接,或者 dataURI。
|
||||||
|
icon: 'diamond', // 标记类型,可选 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
|
||||||
|
offsetCenter: [0, '-28%'], // 相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
|
||||||
|
length: '36%', // 指针长度,可以是绝对数值,也可以是相对于半径的半分比。
|
||||||
|
width: 20, // 指针宽度
|
||||||
|
itemStyle: { // 指针样式
|
||||||
|
// color: 'auto' // 图形颜色
|
||||||
|
// 线性渐变,前四个参数分别是 x0, y0, x2, y2, 范围从 0 - 1,相当于在图形包围盒中的百分比,如果 globalCoord 为 `true`,则该四个值是绝对的像素位置
|
||||||
|
color: gradient.value
|
||||||
|
// borderColor: '#000', // 图形的描边颜色
|
||||||
|
// borderWidth: 3, // 描边线宽。为 0 时无描边
|
||||||
|
// borderType: 'solid', // 描边类型,可选:'solid' 'dashed' 'dotted'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
anchor: { // 表盘中指针的固定点
|
||||||
|
show: true, // 是否显示固定点
|
||||||
|
showAbove: true, // 固定点是否显示在指针上面
|
||||||
|
size: 24, // 固定点大小
|
||||||
|
// 可以通过 'image://url' 设置为图片,其中 URL 为图片的链接,或者 dataURI。
|
||||||
|
icon: 'circle', // 标记类型,可选 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
|
||||||
|
offsetCenter: [0, '16%'], // 相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
|
||||||
|
itemStyle: { // 指针固定点样式
|
||||||
|
color: props.themeColor, // 图形的颜色
|
||||||
|
borderColor: '#eee', // 固定点边框颜色
|
||||||
|
borderWidth: 8, // 描边线宽。为 0 时无描边
|
||||||
|
borderType: 'solid', // 描边类型,可选:'solid' 'dashed' 'dotted'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: { // 仪表盘标题
|
||||||
|
show: true, // 是否显示标题
|
||||||
|
offsetCenter: [0, '36%'], // 相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
|
||||||
|
color: '#464646', // 文字的颜色
|
||||||
|
fontStyle: 'normal', // 文字字体的风格,可选 'normal' 'italic' 'oblique'
|
||||||
|
fontWeight: 'bold', // 文字字体的粗细,可选 'normal' 'bold' 'bolder' 'lighter' 100 | 200 | 300 | 400...
|
||||||
|
fontFamily: 'sans-serif', // 文字的字体系列,还可以是 'serif' , 'monospace', 'Arial', 'Courier New', 'Microsoft YaHei', ...
|
||||||
|
fontSize: 45 // 文字的字体大小
|
||||||
|
// lineHeight: 48, // 行高
|
||||||
|
/*
|
||||||
|
可以使用颜色值,例如:'#123234', 'red', 'rgba(0,23,11,0.3)'
|
||||||
|
也可以直接使用图片,例如:
|
||||||
|
backgroundColor: {
|
||||||
|
image: 'xxx/xxx.png'
|
||||||
|
// 这里可以是图片的 URL,
|
||||||
|
// 或者图片的 dataURI,
|
||||||
|
// 或者 HTMLImageElement 对象,
|
||||||
|
// 或者 HTMLCanvasElement 对象。
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// backgroundColor: 'transparent', // 文字块背景色
|
||||||
|
// borderColor: 'red', // 文字块边框颜色
|
||||||
|
// borderWidth: 3, // 文字块边框宽度
|
||||||
|
// borderType: 'solid', // 文字块边框描边类型,可选 'solid' 'dashed' 'dotted'
|
||||||
|
// borderRadius: 10, // 文字块的圆角
|
||||||
|
// padding: [6, 12], // 文字块的内边距,文字块宽高不包含 padding
|
||||||
|
// width: 60, // 文本显示宽度
|
||||||
|
// height: 60 // 文本显示高度
|
||||||
|
},
|
||||||
|
detail: { // 仪表盘详情,用于显示数据,即表盘中心的数据展示
|
||||||
|
show: true, // 是否显示详情
|
||||||
|
color: props.themeColor, // 文本颜色
|
||||||
|
fontStyle: 'normal', // 文字字体的风格,可选 'normal' 'italic' 'oblique'
|
||||||
|
fontWeight: 'bold', // 文字字体的粗细,可选 'normal' 'bold' 'bolder' 'lighter' 100 | 200 | 300 | 400...
|
||||||
|
fontFamily: 'Microsoft YaHei', // 文字的字体系列,还可以是 'serif' , 'monospace', 'Arial', 'Courier New', 'Microsoft YaHei', ...
|
||||||
|
fontSize: 72, // 文字的字体大小
|
||||||
|
backgroundColor: 'transparent', // 详情背景色
|
||||||
|
// borderColor: '#ccc', // 详情边框颜色
|
||||||
|
// borderWidth: 1, // 详情边框宽度
|
||||||
|
// borderType: 'solid', // 'solid' 'dashed' 'dotted'
|
||||||
|
// borderRadius: 5, // 文字块的圆角
|
||||||
|
// padding: [3, 6], // 文字块的内边距
|
||||||
|
valueAnimation: true, // 是否开启标签的数字动画
|
||||||
|
offsetCenter: [0, '-5%'], // 相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
|
||||||
|
formatter: function (value: number) { // 格式化函数或者字符串
|
||||||
|
return value + '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
系列中的数据内容数组,
|
||||||
|
数组项可以为单个数值:[12, 34, 56, 10, 23]
|
||||||
|
数据项也可为一个对象:[
|
||||||
|
{
|
||||||
|
// 数据项的名称
|
||||||
|
name: '数据1',
|
||||||
|
// 数据项值8
|
||||||
|
value: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '数据2',
|
||||||
|
value: 20
|
||||||
|
}]
|
||||||
|
对象支持的所有属性:{ title , detail , name , value , itemStyle }
|
||||||
|
*/
|
||||||
|
data: props.gaugeData
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
gaugeChart.value.setOption(option);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div ref="chart" :style="`width: ${chartWidth}; height: ${chartHeight};`"></div>
|
||||||
|
</template>
|
||||||
356
src/components/MyUI/Grid/Col.vue
Normal file
356
src/components/MyUI/Grid/Col.vue
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
import {useEventListener} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
span?: number // 栅格占位格数,取 0,1,2...24,为 0 时相当于 display: none,优先级低于 xs, sm, md, lg, xl, xxl
|
||||||
|
offset?: number // 栅格左侧的间隔格数,取 0,1,2...24
|
||||||
|
flex?: string | number // flex 布局填充
|
||||||
|
order?: number // 栅格顺序,取 0,1,2...
|
||||||
|
xs?: number | { span?: number; offset?: number } // <576px 响应式栅格
|
||||||
|
sm?: number | { span?: number; offset?: number } // ≥576px 响应式栅格
|
||||||
|
md?: number | { span?: number; offset?: number } // ≥768px 响应式栅格
|
||||||
|
lg?: number | { span?: number; offset?: number } // ≥992px 响应式栅格
|
||||||
|
xl?: number | { span?: number; offset?: number } // ≥1200px 响应式栅格
|
||||||
|
xxl?: number | { span?: number; offset?: number } // ≥1600px 响应式栅格
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
span: undefined,
|
||||||
|
offset: 0,
|
||||||
|
flex: undefined,
|
||||||
|
order: 0,
|
||||||
|
xs: undefined,
|
||||||
|
sm: undefined,
|
||||||
|
md: undefined,
|
||||||
|
lg: undefined,
|
||||||
|
xl: undefined,
|
||||||
|
xxl: undefined
|
||||||
|
});
|
||||||
|
const flexValue = computed(() => {
|
||||||
|
if (typeof props.flex === 'number') {
|
||||||
|
return `${props.flex} ${props.flex} auto`;
|
||||||
|
}
|
||||||
|
return props.flex;
|
||||||
|
});
|
||||||
|
const responsiveProperties = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
width: 1600,
|
||||||
|
value: props.xxl
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 1200,
|
||||||
|
value: props.xl
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 992,
|
||||||
|
value: props.lg
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 768,
|
||||||
|
value: props.md
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 576,
|
||||||
|
value: props.sm
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 0,
|
||||||
|
value: props.xs
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const viewportWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
|
function getViewportWidth() {
|
||||||
|
viewportWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener(window, 'resize', getViewportWidth);
|
||||||
|
const responsiveValue = computed(() => {
|
||||||
|
for (const responsive of responsiveProperties.value) {
|
||||||
|
if (responsive.value && viewportWidth.value >= responsive.width) {
|
||||||
|
if (typeof responsive.value === 'object') {
|
||||||
|
return {
|
||||||
|
span: responsive.value.span || props.span,
|
||||||
|
offset: responsive.value.offset || props.offset
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
span: responsive.value,
|
||||||
|
offset: props.offset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
span: props.span,
|
||||||
|
offset: props.offset
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="`grid-col col-${responsiveValue.span} offset-${responsiveValue.offset}`"
|
||||||
|
style="padding-left: var(--xGap); padding-right: var(--xGap)"
|
||||||
|
:style="`flex: ${flexValue}; order: ${order};`"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.grid-col {
|
||||||
|
position: relative;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 1px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-0 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-1 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 4.16666666666666%;
|
||||||
|
max-width: 4.16666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-1 {
|
||||||
|
margin-left: 4.16666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-2 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 8.33333333333333%;
|
||||||
|
max-width: 8.33333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-2 {
|
||||||
|
margin-left: 8.33333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-3 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 12.5%;
|
||||||
|
max-width: 12.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-3 {
|
||||||
|
margin-left: 12.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-4 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 16.66666666666666%;
|
||||||
|
max-width: 16.66666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-4 {
|
||||||
|
margin-left: 16.66666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-5 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 20.83333333333333%;
|
||||||
|
max-width: 20.83333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-5 {
|
||||||
|
margin-left: 20.83333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-6 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-6 {
|
||||||
|
margin-left: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-7 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 29.16666666666666%;
|
||||||
|
max-width: 29.16666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-7 {
|
||||||
|
margin-left: 29.16666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-8 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 33.33333333333333%;
|
||||||
|
max-width: 33.33333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-8 {
|
||||||
|
margin-left: 33.33333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-9 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 37.5%;
|
||||||
|
max-width: 37.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-9 {
|
||||||
|
margin-left: 37.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-10 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 41.66666666666666%;
|
||||||
|
max-width: 41.66666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-10 {
|
||||||
|
margin-left: 41.66666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-11 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 45.83333333333333%;
|
||||||
|
max-width: 45.83333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-11 {
|
||||||
|
margin-left: 45.83333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-12 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-12 {
|
||||||
|
margin-left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-13 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 54.16666666666666%;
|
||||||
|
max-width: 54.16666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-13 {
|
||||||
|
margin-left: 54.16666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-14 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 58.33333333333333%;
|
||||||
|
max-width: 58.33333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-14 {
|
||||||
|
margin-left: 58.33333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-15 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 62.5%;
|
||||||
|
max-width: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-15 {
|
||||||
|
margin-left: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-16 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 66.66666666666666%;
|
||||||
|
max-width: 66.66666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-16 {
|
||||||
|
margin-left: 66.6666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-17 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 70.83333333333333%;
|
||||||
|
max-width: 70.83333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-17 {
|
||||||
|
margin-left: 70.83333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-18 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 75%;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-18 {
|
||||||
|
margin-left: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-19 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 79.16666666666666%;
|
||||||
|
max-width: 79.16666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-19 {
|
||||||
|
margin-left: 79.16666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-20 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 83.33333333333333%;
|
||||||
|
max-width: 83.33333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-20 {
|
||||||
|
margin-left: 83.33333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-21 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 87.5%;
|
||||||
|
max-width: 87.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-21 {
|
||||||
|
margin-left: 87.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-22 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 91.66666666666666%;
|
||||||
|
max-width: 91.66666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-22 {
|
||||||
|
margin-left: 91.66666666666666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-23 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 95.83333333333333%;
|
||||||
|
max-width: 95.83333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-23 {
|
||||||
|
margin-left: 95.83333333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-24 {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offset-24 {
|
||||||
|
margin-left: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
110
src/components/MyUI/Grid/Row.vue
Normal file
110
src/components/MyUI/Grid/Row.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useEventListener } from '../Utils';
|
||||||
|
interface Responsive {
|
||||||
|
xs?: number // <576px 响应式栅格
|
||||||
|
sm?: number // ≥576px 响应式栅格
|
||||||
|
md?: number // ≥768px 响应式栅格
|
||||||
|
lg?: number // ≥992px 响应式栅格
|
||||||
|
xl?: number // ≥1200px 响应式栅格
|
||||||
|
xxl?: number // ≥1600px 响应式栅格
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 行宽度,单位 px
|
||||||
|
// 推荐使用 (16+8n)px 作为栅格间隔(n 是自然数:0,1,2,3...)
|
||||||
|
gutter?: number | [number | Responsive, number | Responsive] | Responsive // 栅格间隔,可以写成像素值或支持响应式的对象写法来设置水平间隔 { xs: 8, sm: 16, md: 24}。或者使用数组形式同时设置 [水平间距, 垂直间距]
|
||||||
|
wrap?: boolean // 是否自动换行
|
||||||
|
align?: 'top' | 'middle' | 'bottom' | 'stretch' // 垂直对齐方式
|
||||||
|
justify?: 'start' | 'end' | 'center' | 'space-around' | 'space-between' | 'space-evenly' // 水平排列方式
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 'auto',
|
||||||
|
gutter: 0,
|
||||||
|
wrap: false,
|
||||||
|
align: 'top',
|
||||||
|
justify: 'start'
|
||||||
|
});
|
||||||
|
const alignProperties = {
|
||||||
|
top: 'flex-start',
|
||||||
|
middle: 'center',
|
||||||
|
bottom: 'flex-end',
|
||||||
|
stretch: 'stretch'
|
||||||
|
};
|
||||||
|
const viewportWidth = ref(window.innerWidth);
|
||||||
|
function getViewportWidth() {
|
||||||
|
viewportWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
useEventListener(window, 'resize', getViewportWidth);
|
||||||
|
const xGap = computed(() => {
|
||||||
|
if (typeof props.gutter === 'number') {
|
||||||
|
return props.gutter;
|
||||||
|
}
|
||||||
|
if (Array.isArray(props.gutter)) {
|
||||||
|
if (typeof props.gutter[0] === 'object') {
|
||||||
|
return getResponsiveGap(props.gutter[0]);
|
||||||
|
}
|
||||||
|
return props.gutter[0];
|
||||||
|
}
|
||||||
|
if (typeof props.gutter === 'object') {
|
||||||
|
return getResponsiveGap(props.gutter);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
const yGap = computed(() => {
|
||||||
|
if (Array.isArray(props.gutter)) {
|
||||||
|
if (typeof props.gutter[1] === 'object') {
|
||||||
|
return getResponsiveGap(props.gutter[1]);
|
||||||
|
}
|
||||||
|
return props.gutter[1];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
const rowWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
function getResponsiveGap(gutter: any) {
|
||||||
|
if (viewportWidth.value >= 1600 && gutter.xxl) {
|
||||||
|
return gutter.xxl;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value >= 1200 && gutter.xl) {
|
||||||
|
return gutter.xl;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value >= 992 && gutter.lg) {
|
||||||
|
return gutter.lg;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value >= 768 && gutter.md) {
|
||||||
|
return gutter.md;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value >= 576 && gutter.sm) {
|
||||||
|
return gutter.sm;
|
||||||
|
}
|
||||||
|
if (viewportWidth.value < 576 && gutter.xs) {
|
||||||
|
return gutter.xs;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-grid-row"
|
||||||
|
:class="{ 'gutter-row': gutter }"
|
||||||
|
:style="`--xGap: ${(xGap as number) / 2}px; --justify: ${justify}; --align: ${alignProperties[align]}; width: ${rowWidth}; margin-left: -${(xGap as number) / 2}px; margin-right: -${(xGap as number) / 2}px; row-gap: ${yGap}px;`"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-grid-row {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: var(--justify);
|
||||||
|
align-items: var(--align);
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
841
src/components/MyUI/Image/Image.vue
Normal file
841
src/components/MyUI/Image/Image.vue
Normal file
@@ -0,0 +1,841 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, nextTick, ref, watchEffect} from 'vue';
|
||||||
|
import Space from '../Space/Space.vue';
|
||||||
|
import Spin from '../Spin/Spin.vue';
|
||||||
|
import {add} from '../Utils';
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
src: string // 图像地址
|
||||||
|
name?: string // 图像名称
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
src?: string | Image[] // 图像地址或图像地址数组
|
||||||
|
name?: string // 图像名称,没有传入图片名时自动从图像地址 src 中读取
|
||||||
|
width?: string | number | (string | number)[] // 图像宽度,单位 px
|
||||||
|
height?: string | number | (string | number)[] // 图像高度,单位 px
|
||||||
|
bordered?: boolean // 是否显示边框
|
||||||
|
fit?: 'contain' | 'fill' | 'cover' | 'none' | 'scale-down' // 图片在容器内的的适应类型
|
||||||
|
preview?: string // 预览文本 string | slot
|
||||||
|
spaceProps?: object // Space 组件属性配置,用于配置多张展示图片时的排列方式
|
||||||
|
spinProps?: object // Spin 组件属性配置,用于配置图片加载中样式
|
||||||
|
zoomRatio?: number // 每次缩放比率
|
||||||
|
minZoomScale?: number // 最小缩放比例
|
||||||
|
maxZoomScale?: number // 最大缩放比例
|
||||||
|
resetOnDbclick?: boolean // 缩放移动旋转图片后,是否可以双击还原
|
||||||
|
loop?: boolean // 是否可以循环切换图片
|
||||||
|
album?: boolean // 是否相册模式,即从一张展示图片点开相册
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
src: undefined,
|
||||||
|
name: undefined,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
bordered: true,
|
||||||
|
fit: 'contain',
|
||||||
|
preview: '预览',
|
||||||
|
spaceProps: () => ({}),
|
||||||
|
spinProps: () => ({}),
|
||||||
|
zoomRatio: 0.1,
|
||||||
|
minZoomScale: 0.1,
|
||||||
|
maxZoomScale: 10,
|
||||||
|
resetOnDbclick: true,
|
||||||
|
loop: false,
|
||||||
|
album: false
|
||||||
|
});
|
||||||
|
const images = ref<any[]>([]);
|
||||||
|
const previewRef = ref(); // DOM 引用
|
||||||
|
const previewIndex = ref(0); // 当前预览的图片索引
|
||||||
|
const showPreview = ref(false); // 是否显示预览
|
||||||
|
const rotate = ref(0); // 预览图片旋转角度
|
||||||
|
const scale = ref(1); // 缩放比例
|
||||||
|
const swapX = ref(1); // 水平镜像数值符号
|
||||||
|
const swapY = ref(1); // 垂直镜像数值符号
|
||||||
|
const sourceX = ref(0); // 拖动开始时位置
|
||||||
|
const sourceY = ref(0); // 拖动开始时位置
|
||||||
|
const dragX = ref(0); // 拖动横向距离
|
||||||
|
const dragY = ref(0); // 拖动纵向距离
|
||||||
|
const imageAmount = computed(() => {
|
||||||
|
return images.value.length;
|
||||||
|
});
|
||||||
|
const complete = ref(Array(imageAmount.value).fill(false)); // 图片是否加载完成
|
||||||
|
const loaded = ref(Array(imageAmount.value).fill(false)); // 预览图片是否加载完成
|
||||||
|
watchEffect(() => {
|
||||||
|
images.value = getImages();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getImages() {
|
||||||
|
if (Array.isArray(props.src)) {
|
||||||
|
return props.src;
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
src: props.src,
|
||||||
|
name: props.name
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onComplete(n: number) {
|
||||||
|
// 图片加载完成
|
||||||
|
complete.value[n] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoaded(index: number) {
|
||||||
|
// 预览图片加载完成
|
||||||
|
loaded.value[index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageName(image: Image) {
|
||||||
|
// 从图像地址src中获取图像名称
|
||||||
|
if (image) {
|
||||||
|
if (image.name) {
|
||||||
|
return image.name;
|
||||||
|
} else {
|
||||||
|
const res = image.src.split('?')[0].split('/');
|
||||||
|
return res[res.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageSize(size: string | number | (string | number)[], index: number): string {
|
||||||
|
if (Array.isArray(size)) {
|
||||||
|
if (typeof size[index] === 'number') {
|
||||||
|
return `${size[index]}px`;
|
||||||
|
}
|
||||||
|
return size[index];
|
||||||
|
} else {
|
||||||
|
if (typeof size === 'number') {
|
||||||
|
return `${size}px`;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyboard(e: KeyboardEvent) {
|
||||||
|
if (showPreview.value && imageAmount.value > 1) {
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
onSwitchLeft();
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
|
onSwitchRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onPreview(index: number) {
|
||||||
|
scale.value = 1;
|
||||||
|
rotate.value = 0;
|
||||||
|
dragX.value = 0;
|
||||||
|
dragY.value = 0;
|
||||||
|
showPreview.value = true;
|
||||||
|
previewIndex.value = index;
|
||||||
|
await nextTick();
|
||||||
|
previewRef.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
preview: onPreview
|
||||||
|
});
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
// 关闭
|
||||||
|
showPreview.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onZoomin() {
|
||||||
|
// 放大
|
||||||
|
if (scale.value + props.zoomRatio > props.maxZoomScale) {
|
||||||
|
scale.value = props.maxZoomScale;
|
||||||
|
} else {
|
||||||
|
scale.value = add(scale.value, props.zoomRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onZoomout() {
|
||||||
|
// 缩小
|
||||||
|
if (scale.value - props.zoomRatio < props.minZoomScale) {
|
||||||
|
scale.value = props.minZoomScale;
|
||||||
|
} else {
|
||||||
|
scale.value = add(scale.value, -props.zoomRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResetOrigin() {
|
||||||
|
// 重置图片为初始状态
|
||||||
|
scale.value = 1;
|
||||||
|
swapX.value = 1;
|
||||||
|
swapY.value = 1;
|
||||||
|
rotate.value = 0;
|
||||||
|
dragX.value = 0;
|
||||||
|
dragY.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClockwiseRotate() {
|
||||||
|
// 顺时针旋转
|
||||||
|
rotate.value += 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAnticlockwiseRotate() {
|
||||||
|
// 逆时针旋转
|
||||||
|
rotate.value -= 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHorizontalMirror() {
|
||||||
|
swapX.value *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVerticalMirror() {
|
||||||
|
swapY.value *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWheel(e: WheelEvent) {
|
||||||
|
// 鼠标滚轮缩放
|
||||||
|
// e.preventDefault() // 禁止浏览器捕获滑动事件
|
||||||
|
const scrollZoom = e.deltaY * props.zoomRatio * 0.1; // 滚轮的纵向滚动量
|
||||||
|
if (scale.value === props.minZoomScale && scrollZoom > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scale.value === props.maxZoomScale && scrollZoom < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scale.value - scrollZoom < props.minZoomScale) {
|
||||||
|
scale.value = props.minZoomScale;
|
||||||
|
} else if (scale.value - scrollZoom > props.maxZoomScale) {
|
||||||
|
scale.value = props.maxZoomScale;
|
||||||
|
} else {
|
||||||
|
scale.value = add(scale.value, -scrollZoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(event: MouseEvent) {
|
||||||
|
// event.preventDefault() // 消除拖动元素时的阴影
|
||||||
|
const el = event.target; // 当前点击的元素
|
||||||
|
const imageRect = (el as Element).getBoundingClientRect();
|
||||||
|
const top = imageRect.top; // 图片上边缘距浏览器窗口上边界的距离
|
||||||
|
const bottom = imageRect.bottom; // 图片下边缘距浏览器窗口上边界的距离
|
||||||
|
const right = imageRect.right; // 图片右边缘距浏览器窗口左边界的距离
|
||||||
|
const left = imageRect.left; // 图片左边缘距浏览器窗口左边界的距离
|
||||||
|
const viewportWidth = window.innerWidth; // 视口宽度
|
||||||
|
const viewportHeight = window.innerHeight; // 视口高度
|
||||||
|
sourceX.value = event.clientX; // 鼠标按下时相对于视口左边缘的X坐标
|
||||||
|
sourceY.value = event.clientY; // 鼠标按下时相对于视口上边缘的Y坐标
|
||||||
|
const sourceDragX = dragX.value; // 鼠标按下时图片的X轴偏移量
|
||||||
|
const sourceDragY = dragY.value; // 鼠标按下时图片的Y轴偏移量
|
||||||
|
window.onmousemove = (e: MouseEvent) => {
|
||||||
|
// e.clientX返回事件被触发时鼠标指针相对于浏览器可视窗口的水平坐标
|
||||||
|
dragX.value = sourceDragX + e.clientX - sourceX.value;
|
||||||
|
dragY.value = sourceDragY + e.clientY - sourceY.value;
|
||||||
|
};
|
||||||
|
window.onmouseup = () => {
|
||||||
|
if (dragX.value > sourceDragX + viewportWidth - right) {
|
||||||
|
// 溢出视口右边缘
|
||||||
|
dragX.value = sourceDragX + viewportWidth - right;
|
||||||
|
}
|
||||||
|
if (dragX.value < sourceDragX - left) {
|
||||||
|
// 溢出视口左边缘
|
||||||
|
dragX.value = sourceDragX - left;
|
||||||
|
}
|
||||||
|
if (dragY.value > sourceDragY + viewportHeight - bottom) {
|
||||||
|
// 溢出视口下边缘
|
||||||
|
dragY.value = sourceDragY + viewportHeight - bottom;
|
||||||
|
}
|
||||||
|
if (dragY.value < sourceDragY - top) {
|
||||||
|
// 溢出视口上边缘
|
||||||
|
dragY.value = sourceDragY - top;
|
||||||
|
}
|
||||||
|
window.onmousemove = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSwitchLeft() {
|
||||||
|
if (props.loop) {
|
||||||
|
previewIndex.value = (previewIndex.value - 1 + imageAmount.value) % imageAmount.value;
|
||||||
|
} else {
|
||||||
|
if (previewIndex.value > 0) {
|
||||||
|
previewIndex.value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onResetOrigin();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSwitchRight() {
|
||||||
|
if (props.loop) {
|
||||||
|
previewIndex.value = (previewIndex.value + 1) % imageAmount.value;
|
||||||
|
} else {
|
||||||
|
if (previewIndex.value < imageAmount.value - 1) {
|
||||||
|
previewIndex.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onResetOrigin();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-image-wrap">
|
||||||
|
<Space gap="small" v-bind="spaceProps">
|
||||||
|
<div
|
||||||
|
v-show="!album || (album && index === 0)"
|
||||||
|
class="m-image"
|
||||||
|
:class="{ 'image-bordered': bordered, 'image-hover-mask': complete[index] }"
|
||||||
|
:style="`width: ${getImageSize(props.width, index)}; height: ${getImageSize(props.height, index)};`"
|
||||||
|
v-for="(image, index) in images"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<Spin :spinning="!complete[index]" indicator="dynamic-circle" size="small" v-bind="spinProps">
|
||||||
|
<img
|
||||||
|
class="u-image"
|
||||||
|
:style="`object-fit: ${fit};`"
|
||||||
|
@load="onComplete(index)"
|
||||||
|
:src="image.src"
|
||||||
|
:alt="getImageName(image)"
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
<div class="m-image-mask" @click="onPreview(index)">
|
||||||
|
<div class="image-mask-info">
|
||||||
|
<svg
|
||||||
|
class="eye-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="eye"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<p class="mask-pre">
|
||||||
|
<slot name="preview">{{ preview }}</slot>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-show="showPreview" class="m-preview-mask"></div>
|
||||||
|
</Transition>
|
||||||
|
<Transition name="zoom">
|
||||||
|
<div
|
||||||
|
v-show="showPreview"
|
||||||
|
ref="previewRef"
|
||||||
|
class="m-preview-wrap"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.self="onClose"
|
||||||
|
@wheel.prevent="onWheel"
|
||||||
|
@keydown="onKeyboard"
|
||||||
|
@keydown.esc="onClose"
|
||||||
|
>
|
||||||
|
<div class="m-preview-body">
|
||||||
|
<div class="m-preview-operations">
|
||||||
|
<a
|
||||||
|
class="previe-name"
|
||||||
|
:href="images[previewIndex].src"
|
||||||
|
target="_blank"
|
||||||
|
:title="getImageName(images[previewIndex])"
|
||||||
|
>
|
||||||
|
{{ getImageName(images[previewIndex]) }}
|
||||||
|
</a>
|
||||||
|
<p class="preview-progress" v-show="Array.isArray(src)">{{ previewIndex + 1 }} / {{ imageAmount }}</p>
|
||||||
|
<div class="preview-operation" title="关闭" @click="onClose">
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="preview-operation"
|
||||||
|
:class="{ 'operation-disabled': scale === maxZoomScale }"
|
||||||
|
title="放大"
|
||||||
|
@click="onZoomin"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="zoom-in"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="preview-operation"
|
||||||
|
:class="{ 'operation-disabled': scale === minZoomScale }"
|
||||||
|
title="缩小"
|
||||||
|
@click="onZoomout"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="zoom-out"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="preview-operation" title="还原" @click="onResetOrigin">
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="expand"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M342 88H120c-17.7 0-32 14.3-32 32v224c0 8.8 7.2 16 16 16h48c8.8 0 16-7.2 16-16V168h174c8.8 0 16-7.2 16-16v-48c0-8.8-7.2-16-16-16zm578 576h-48c-8.8 0-16 7.2-16 16v176H682c-8.8 0-16 7.2-16 16v48c0 8.8 7.2 16 16 16h222c17.7 0 32-14.3 32-32V680c0-8.8-7.2-16-16-16zM342 856H168V680c0-8.8-7.2-16-16-16h-48c-8.8 0-16 7.2-16 16v224c0 17.7 14.3 32 32 32h222c8.8 0 16-7.2 16-16v-48c0-8.8-7.2-16-16-16zM904 88H682c-8.8 0-16 7.2-16 16v48c0 8.8 7.2 16 16 16h174v176c0 8.8 7.2 16 16 16h48c8.8 0 16-7.2 16-16V120c0-17.7-14.3-32-32-32z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="preview-operation" title="向右旋转" @click="onClockwiseRotate">
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="rotate-right"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2A399.75 399.75 0 00123.5 631h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H396V494h440v326z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="preview-operation" title="向左旋转" @click="onAnticlockwiseRotate">
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="rotate-left"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H188V494h440v326z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8a325.95 325.95 0 016.5 140.9h74.9c14.8-103.6-11.3-213-81-302.3z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="preview-operation" title="水平镜像" @click="onHorizontalMirror">
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="swap"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M847.9 592H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h605.2L612.9 851c-4.1 5.2-.4 13 6.3 13h72.5c4.9 0 9.5-2.2 12.6-6.1l168.8-214.1c16.5-21 1.6-51.8-25.2-51.8zM872 356H266.8l144.3-183c4.1-5.2.4-13-6.3-13h-72.5c-4.9 0-9.5 2.2-12.6 6.1L150.9 380.2c-16.5 21-1.6 51.8 25.1 51.8h696c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="preview-operation" title="垂直镜像" @click="onVerticalMirror">
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
style="transform: rotate(90deg)"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="swap"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M847.9 592H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h605.2L612.9 851c-4.1 5.2-.4 13 6.3 13h72.5c4.9 0 9.5-2.2 12.6-6.1l168.8-214.1c16.5-21 1.6-51.8-25.2-51.8zM872 356H266.8l144.3-183c4.1-5.2.4-13-6.3-13h-72.5c-4.9 0-9.5 2.2-12.6 6.1L150.9 380.2c-16.5 21-1.6 51.8 25.1 51.8h696c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="m-preview-image"
|
||||||
|
:style="`transform: translate3d(${dragX}px, ${dragY}px, 0px);`"
|
||||||
|
v-show="previewIndex === index"
|
||||||
|
v-for="(image, index) in images"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="preview-image"
|
||||||
|
:style="`transform: scale3d(${swapX * scale}, ${swapY * scale}, 1) rotate(${rotate}deg);`"
|
||||||
|
:src="image.src"
|
||||||
|
:alt="getImageName(image)"
|
||||||
|
@mousedown.prevent="onMouseDown($event)"
|
||||||
|
@load="onLoaded(index)"
|
||||||
|
@dblclick="resetOnDbclick ? onResetOrigin() : () => false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template v-if="imageAmount > 1">
|
||||||
|
<div class="switch-left" :class="{ 'switch-disabled': previewIndex === 0 && !loop }" @click="onSwitchLeft">
|
||||||
|
<svg
|
||||||
|
class="switch-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="left"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="switch-right"
|
||||||
|
:class="{ 'switch-disabled': previewIndex === imageAmount - 1 && !loop }"
|
||||||
|
@click="onSwitchRight"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="switch-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="right"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-enter-active {
|
||||||
|
transition: opacity 0.3s cubic-bezier(0.08, 0.82, 0.17, 1),
|
||||||
|
transform 0.3s cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-leave-active {
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.78, 0.14, 0.15, 0.86),
|
||||||
|
transform 0.2s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-enter-from,
|
||||||
|
.zoom-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-image-wrap {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.image-hover-mask {
|
||||||
|
&:hover {
|
||||||
|
.m-image-mask {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-image {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.u-image {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-image-mask {
|
||||||
|
// top right bottom left 简写为 inset: 0
|
||||||
|
// insert 无论元素的书写模式、行内方向和文本朝向如何,其所定义的都不是逻辑偏移而是实体偏移
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
.image-mask-info {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
.eye-svg {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask-pre {
|
||||||
|
display: inline-block;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-bordered {
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-preview-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-preview-wrap {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
overflow: auto;
|
||||||
|
outline: none;
|
||||||
|
z-index: 1080;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.m-preview-body {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.m-preview-operations {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 9;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
height: 42px;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.previe-name {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
line-height: 1.57;
|
||||||
|
max-width: calc(50% - 60px);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-progress {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
line-height: 1.57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-operation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-svg {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #fff;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-disabled {
|
||||||
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.icon-svg {
|
||||||
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-preview-image {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
inset: 0;
|
||||||
|
transition: transform 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
cursor: grab;
|
||||||
|
transition: transform 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-left {
|
||||||
|
left: 12px;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 1081;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-top: -20px;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
pointer-events: auto;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-svg {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #fff;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-right {
|
||||||
|
right: 12px;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 1081;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-top: -20px;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-svg {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #fff;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-disabled {
|
||||||
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
background: transparent;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-svg {
|
||||||
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
425
src/components/MyUI/Input/Input.vue
Normal file
425
src/components/MyUI/Input/Input.vue
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, nextTick, ref} from 'vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 输入框宽度,单位 px
|
||||||
|
size?: 'small' | 'middle' | 'large' // 输入框大小
|
||||||
|
addonBefore?: string // 设置前置标签 string | slot
|
||||||
|
addonAfter?: string // 设置后置标签 string | slot
|
||||||
|
prefix?: string // 前缀图标 string | slot
|
||||||
|
suffix?: string // 后缀图标 string | slot
|
||||||
|
allowClear?: boolean // 可以点击清除图标删除内容
|
||||||
|
password?: boolean // 是否启用密码框
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
placeholder?: string // 文本输入的占位符
|
||||||
|
maxLength?: number // 最大长度
|
||||||
|
showCount?: boolean // 是否展示字数
|
||||||
|
value?: string // (v-model) 输入框内容
|
||||||
|
valueModifiers?: object // 用于访问组件的 v-model 上添加的修饰符
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: '100%',
|
||||||
|
size: 'middle',
|
||||||
|
addonBefore: undefined,
|
||||||
|
addonAfter: undefined,
|
||||||
|
prefix: undefined,
|
||||||
|
suffix: undefined,
|
||||||
|
allowClear: false,
|
||||||
|
password: false,
|
||||||
|
disabled: false,
|
||||||
|
placeholder: undefined,
|
||||||
|
maxlength: undefined,
|
||||||
|
showCount: false,
|
||||||
|
value: undefined,
|
||||||
|
valueModifiers: () => ({})
|
||||||
|
});
|
||||||
|
const inputRef = ref(); // input 元素引用
|
||||||
|
const showPassword = ref(false);
|
||||||
|
const emits = defineEmits(['update:value', 'change', 'enter']);
|
||||||
|
const slotsExist = useSlotsExist(['prefix', 'suffix', 'addonBefore', 'addonAfter']);
|
||||||
|
const inputWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const showClear = computed(() => {
|
||||||
|
return !props.disabled && props.allowClear;
|
||||||
|
});
|
||||||
|
const showCountNum = computed(() => {
|
||||||
|
if (props.maxLength) {
|
||||||
|
return `${(props.value ? props.value.length : 0)} / ${props.maxLength}`;
|
||||||
|
}
|
||||||
|
return props.value ? props.value.length : 0;
|
||||||
|
});
|
||||||
|
const showPrefix = computed(() => {
|
||||||
|
return slotsExist.prefix || props.prefix;
|
||||||
|
});
|
||||||
|
const showSuffix = computed(() => {
|
||||||
|
return slotsExist.suffix || props.suffix;
|
||||||
|
});
|
||||||
|
const showInputSuffix = computed(() => {
|
||||||
|
return showClear.value || props.password || props.showCount || showSuffix.value;
|
||||||
|
});
|
||||||
|
const showBefore = computed(() => {
|
||||||
|
return slotsExist.addonBefore || props.addonBefore;
|
||||||
|
});
|
||||||
|
const showAfter = computed(() => {
|
||||||
|
return slotsExist.addonAfter || props.addonAfter;
|
||||||
|
});
|
||||||
|
const lazyInput = computed(() => {
|
||||||
|
return 'lazy' in props.valueModifiers;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onInput(e: InputEvent) {
|
||||||
|
if (!lazyInput.value) {
|
||||||
|
emits('update:value', (e.target as HTMLInputElement).value);
|
||||||
|
emits('change', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(e: InputEvent) {
|
||||||
|
if (lazyInput.value) {
|
||||||
|
emits('update:value', (e.target as HTMLInputElement).value);
|
||||||
|
emits('change', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyboard(e: KeyboardEvent) {
|
||||||
|
emits('enter', e);
|
||||||
|
if (lazyInput.value) {
|
||||||
|
inputRef.value.blur();
|
||||||
|
nextTick(() => {
|
||||||
|
inputRef.value.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
emits('update:value', '');
|
||||||
|
inputRef.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPassword() {
|
||||||
|
showPassword.value = !showPassword.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-input-wrap" :style="`width: ${inputWidth};`">
|
||||||
|
<span v-if="showBefore" class="m-addon" :class="{ 'addon-before': showBefore }">
|
||||||
|
<slot name="addonBefore">{{ addonBefore }}</slot>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
tabindex="1"
|
||||||
|
class="m-input"
|
||||||
|
:class="[
|
||||||
|
`input-${size}`,
|
||||||
|
{
|
||||||
|
'input-before': showBefore,
|
||||||
|
'input-after': showAfter,
|
||||||
|
'input-disabled': disabled
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span v-if="showPrefix" class="input-prefix">
|
||||||
|
<slot name="prefix">{{ prefix }}</slot>
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
:ref="inputRef"
|
||||||
|
class="u-input"
|
||||||
|
:type="password && !showPassword ? 'password' : 'text'"
|
||||||
|
:value="value"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:maxLength="maxLength"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
|
@keydown.enter.prevent="onKeyboard"
|
||||||
|
/>
|
||||||
|
<span v-if="showInputSuffix" class="input-suffix">
|
||||||
|
<span v-if="showClear" class="m-actions" :class="{ 'clear-hidden': !value }" @click="onClear">
|
||||||
|
<svg
|
||||||
|
class="clear-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span v-if="password" class="m-actions" @click="onPassword">
|
||||||
|
<svg
|
||||||
|
v-show="showPassword"
|
||||||
|
class="eye-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="eye"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-show="!showPassword"
|
||||||
|
class="eye-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="eye-invisible"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span v-if="showCount" class="input-count">{{ showCountNum }}</span>
|
||||||
|
<span v-if="showSuffix" class="m-suffix">
|
||||||
|
<slot name="suffix">{{ suffix }}</slot>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="showAfter" class="m-addon" :class="{ 'addon-after': showAfter }">
|
||||||
|
<slot name="addonAfter">{{ addonAfter }}</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-input-wrap {
|
||||||
|
width: 100%;
|
||||||
|
text-align: start;
|
||||||
|
vertical-align: top;
|
||||||
|
position: relative;
|
||||||
|
display: inline-table;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
|
||||||
|
.m-addon {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 11px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1;
|
||||||
|
display: table-cell;
|
||||||
|
width: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-before {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-after {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-input {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #4096ff;
|
||||||
|
border-right-width: 1px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #4096ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
||||||
|
border-right-width: 1px;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-prefix {
|
||||||
|
margin-right: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-input {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-webkit-input-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:-moz-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-moz-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:-ms-input-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-suffix {
|
||||||
|
margin-left: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.m-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.clear-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
fill: currentColor;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
fill: currentColor;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-count {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-suffix {
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-small {
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-middle {
|
||||||
|
padding: 4px 11px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-large {
|
||||||
|
padding: 7px 11px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.u-input {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-before {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-after {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-input {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
435
src/components/MyUI/InputSearch/InputSearch.vue
Normal file
435
src/components/MyUI/InputSearch/InputSearch.vue
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, nextTick, ref} from 'vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 搜索框宽度,单位 px
|
||||||
|
icon?: boolean // 搜索图标 boolean | slot
|
||||||
|
search?: string // 搜索按钮,默认时为搜索图标 string | slot
|
||||||
|
searchProps?: object // 设置搜索按钮的属性,参考 Button Props
|
||||||
|
size?: 'small' | 'middle' | 'large' // 搜索框大小
|
||||||
|
allowClear?: boolean // 可以点击清除图标删除搜索框内容
|
||||||
|
addonBefore?: string // 设置前置标签 string | slot
|
||||||
|
prefix?: string // 前缀图标 string | slot
|
||||||
|
suffix?: string // 后缀图标 string | slot
|
||||||
|
loading?: boolean // 是否搜索中
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
placeholder?: string // 搜索框输入的占位符
|
||||||
|
maxlength?: number // 文本最大长度
|
||||||
|
showCount?: boolean // 是否展示字数
|
||||||
|
value?: string // (v-model) 搜索框内容
|
||||||
|
valueModifiers?: object // 用于访问组件的 v-model 上添加的修饰符
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: '100%',
|
||||||
|
icon: true,
|
||||||
|
search: undefined,
|
||||||
|
searchProps: () => ({}),
|
||||||
|
size: 'middle',
|
||||||
|
addonBefore: undefined,
|
||||||
|
prefix: undefined,
|
||||||
|
suffix: undefined,
|
||||||
|
allowClear: false,
|
||||||
|
loading: false,
|
||||||
|
disabled: false,
|
||||||
|
placeholder: undefined,
|
||||||
|
maxlength: undefined,
|
||||||
|
showCount: false,
|
||||||
|
value: undefined,
|
||||||
|
valueModifiers: () => ({})
|
||||||
|
});
|
||||||
|
const inputRef = ref();
|
||||||
|
const slotsExist = useSlotsExist(['prefix', 'suffix', 'addonBefore']);
|
||||||
|
const emits = defineEmits(['update:value', 'change', 'search']);
|
||||||
|
const inputSearchWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const showClear = computed(() => {
|
||||||
|
return !props.disabled && props.allowClear;
|
||||||
|
});
|
||||||
|
const showCountNum = computed(() => {
|
||||||
|
if (props.maxlength) {
|
||||||
|
return (props.value ? props.value.length : 0) + ' / ' + props.maxlength;
|
||||||
|
}
|
||||||
|
return props.value ? props.value.length : 0;
|
||||||
|
});
|
||||||
|
const showPrefix = computed(() => {
|
||||||
|
return slotsExist.prefix || props.prefix;
|
||||||
|
});
|
||||||
|
const showSuffix = computed(() => {
|
||||||
|
return slotsExist.suffix || props.suffix;
|
||||||
|
});
|
||||||
|
const showInputSuffix = computed(() => {
|
||||||
|
return showClear.value || props.showCount || showSuffix.value;
|
||||||
|
});
|
||||||
|
const showBefore = computed(() => {
|
||||||
|
return slotsExist.addonBefore || props.addonBefore;
|
||||||
|
});
|
||||||
|
const lazyInput = computed(() => {
|
||||||
|
return 'lazy' in props.valueModifiers;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onInput(e: InputEvent) {
|
||||||
|
if (!lazyInput.value) {
|
||||||
|
emits('update:value', (e.target as HTMLInputElement).value);
|
||||||
|
emits('change', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(e: InputEvent) {
|
||||||
|
if (lazyInput.value) {
|
||||||
|
emits('update:value', (e.target as HTMLInputElement).value);
|
||||||
|
emits('change', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
emits('update:value', '');
|
||||||
|
inputRef.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onInputSearch(_e: KeyboardEvent) {
|
||||||
|
if (!lazyInput.value) {
|
||||||
|
onSearch();
|
||||||
|
} else {
|
||||||
|
if (lazyInput.value) {
|
||||||
|
inputRef.value.blur();
|
||||||
|
await nextTick();
|
||||||
|
inputRef.value.focus();
|
||||||
|
}
|
||||||
|
emits('search', props.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
emits('search', props.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="m-input-search-wrap" :style="`width: ${inputSearchWidth};`">
|
||||||
|
<span v-if="showBefore" class="m-addon-before" :class="`addon-before-${size}`">
|
||||||
|
<slot name="addonBefore">{{ addonBefore }}</slot>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
tabindex="1"
|
||||||
|
class="m-input-search"
|
||||||
|
:class="[
|
||||||
|
`input-search-${size}`,
|
||||||
|
{
|
||||||
|
'input-search-before': showBefore,
|
||||||
|
'input-search-disabled': disabled
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span v-if="showPrefix" class="m-prefix">
|
||||||
|
<slot name="prefix">{{ prefix }}</slot>
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
ref="inputRef"
|
||||||
|
class="input-search"
|
||||||
|
type="text"
|
||||||
|
:value="value"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
|
@keydown.enter.prevent="onInputSearch"
|
||||||
|
/>
|
||||||
|
<span v-if="showInputSuffix" class="input-search-suffix">
|
||||||
|
<span v-if="showClear" class="m-clear" :class="{ 'clear-hidden': !value }" @click="onClear">
|
||||||
|
<svg
|
||||||
|
class="clear-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span v-if="showCount" class="input-search-count">{{ showCountNum }}</span>
|
||||||
|
<span v-if="showSuffix" class="m-suffix">
|
||||||
|
<slot name="suffix">{{ suffix }}</slot>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="m-search-button" @click="onSearch" @keydown.enter.prevent="onSearch">
|
||||||
|
<slot name="search">
|
||||||
|
<Button class="search-btn" :size="size" :disabled="disabled" :loading="loading" v-bind="searchProps">
|
||||||
|
<template v-if="icon" #icon>
|
||||||
|
<svg
|
||||||
|
focusable="false"
|
||||||
|
data-icon="search"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
{{ search }}
|
||||||
|
</Button>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-input-search-wrap {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.m-addon-before {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 11px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-right: 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-before-small {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-before-middle {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-before-small {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-input-search {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #4096ff;
|
||||||
|
border-right-width: 1px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #4096ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
||||||
|
border-right-width: 1px;
|
||||||
|
outline: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-prefix {
|
||||||
|
margin-right: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search {
|
||||||
|
font-size: 14px;
|
||||||
|
color: inherit;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-webkit-input-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:-moz-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-moz-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:-ms-input-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search-suffix {
|
||||||
|
margin-left: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.m-clear {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.clear-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
fill: currentColor;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search-count {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-suffix {
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search-small {
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search-middle {
|
||||||
|
height: 32px;
|
||||||
|
padding: 4px 11px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search-large {
|
||||||
|
height: 40px;
|
||||||
|
padding: 7px 11px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
|
.input-search {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search-before {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search-disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-search-button {
|
||||||
|
position: relative;
|
||||||
|
left: -1px;
|
||||||
|
border-left: 0;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
:deep(.m-btn) {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
|
||||||
|
&:not(.btn-primary):not(.btn-danger):not(.btn-link):not(.btn-disabled) {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
197
src/components/MyUI/List/List.vue
Normal file
197
src/components/MyUI/List/List.vue
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import Spin from '../Spin/Spin.vue';
|
||||||
|
import Empty from '../Empty/Empty.vue';
|
||||||
|
import Pagination from '../Pagination/Pagination.vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bordered?: boolean // 是否展示边框
|
||||||
|
vertical?: boolean // 是否使用竖直样式
|
||||||
|
split?: boolean // 是否展示分割线
|
||||||
|
size?: 'small' | 'middle' | 'large' // 列表尺寸
|
||||||
|
loading?: boolean // 是否加载中
|
||||||
|
hoverable?: boolean // 是否显示悬浮样式
|
||||||
|
header?: string // 列表头部 string | slot
|
||||||
|
footer?: string // 列表底部 string | slot
|
||||||
|
spinProps?: object // Spin 组件属性配置,参考 Spin Props,用于配置列表加载中样式
|
||||||
|
emptyProps?: object // Empty 组件属性配置,参考 Empty Props,用于配置暂无数据样式
|
||||||
|
showPagination?: boolean // 是否显示分页
|
||||||
|
pagination?: object // Pagination 组件属性配置,参考 Pagination Props,用于配置分页功能
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
bordered: false,
|
||||||
|
vertical: false,
|
||||||
|
split: true,
|
||||||
|
size: 'middle',
|
||||||
|
loading: false,
|
||||||
|
hoverable: false,
|
||||||
|
header: undefined,
|
||||||
|
footer: undefined,
|
||||||
|
spinProps: () => ({}),
|
||||||
|
emptyProps: () => ({}),
|
||||||
|
showPagination: false,
|
||||||
|
pagination: () => ({})
|
||||||
|
});
|
||||||
|
const slotsExist = useSlotsExist(['header', 'default', 'footer']);
|
||||||
|
const showHeader = computed(() => {
|
||||||
|
return slotsExist.header || props.header;
|
||||||
|
});
|
||||||
|
const showFooter = computed(() => {
|
||||||
|
return slotsExist.footer || props.footer;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Spin size="small" :spinning="loading" v-bind="spinProps">
|
||||||
|
<div
|
||||||
|
class="m-list"
|
||||||
|
:class="{
|
||||||
|
'list-bordered': bordered,
|
||||||
|
'list-vertical': vertical,
|
||||||
|
'list-split': split,
|
||||||
|
'list-small': size === 'small',
|
||||||
|
'list-large': size === 'large',
|
||||||
|
'list-hoverable': hoverable
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div v-if="showHeader" class="list-header">
|
||||||
|
<slot name="header">{{ header }}</slot>
|
||||||
|
</div>
|
||||||
|
<slot v-if="slotsExist.default"></slot>
|
||||||
|
<div v-else class="list-empty">
|
||||||
|
<Empty image="outlined" v-bind="emptyProps"/>
|
||||||
|
</div>
|
||||||
|
<div v-if="showFooter" class="list-footer">
|
||||||
|
<slot name="footer">{{ footer }}</slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="showPagination" class="list-pagination">
|
||||||
|
<Pagination placement="right" v-bind="pagination"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-list {
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
|
||||||
|
.list-header,
|
||||||
|
.list-footer {
|
||||||
|
background: transparent;
|
||||||
|
padding: 12px 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-empty {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-pagination {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-bordered {
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.list-header,
|
||||||
|
:deep(.m-list-item),
|
||||||
|
.list-footer {
|
||||||
|
padding-inline: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-pagination {
|
||||||
|
margin: 16px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-vertical {
|
||||||
|
:deep(.m-list-item) {
|
||||||
|
align-items: initial;
|
||||||
|
|
||||||
|
.m-list-item-main {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.m-list-item-meta {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.m-list-item-content {
|
||||||
|
.list-item-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-split {
|
||||||
|
.list-header {
|
||||||
|
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.m-list-item) {
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-small {
|
||||||
|
:deep(.m-list-item) {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-bordered.list-small {
|
||||||
|
.list-header,
|
||||||
|
:deep(.m-list-item),
|
||||||
|
.list-footer {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-large {
|
||||||
|
:deep(.m-list-item) {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-bordered.list-large {
|
||||||
|
.list-header,
|
||||||
|
:deep(.m-list-item),
|
||||||
|
.list-footer {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-hoverable {
|
||||||
|
:deep(.m-list-item) {
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
169
src/components/MyUI/LoadingBar/LoadingBar.vue
Normal file
169
src/components/MyUI/LoadingBar/LoadingBar.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {nextTick, ref} from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
containerClass?: string // 加载条容器的类名
|
||||||
|
containerStyle?: CSSProperties // 加载条容器的样式
|
||||||
|
loadingBarSize?: number // 加载条大小,单位 px
|
||||||
|
colorLoading?: string // 加载中颜色
|
||||||
|
colorFinish?: string // 加载完成颜色
|
||||||
|
colorError?: string // 加载错误颜色
|
||||||
|
to?: string | HTMLElement // 加载条的挂载位置,可选:元素标签名(例如 body)或者元素本身
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
containerClass: undefined,
|
||||||
|
containerStyle: () => ({}),
|
||||||
|
loadingBarSize: 2,
|
||||||
|
colorLoading: '#1677ff',
|
||||||
|
colorFinish: '#1677ff',
|
||||||
|
colorError: '#ff4d4f',
|
||||||
|
to: 'body'
|
||||||
|
});
|
||||||
|
const showLoadingBar = ref(false);
|
||||||
|
const loadingBarRef = ref(); // 加载条 DOM 引用
|
||||||
|
const loadingStarted = ref(false); // 加载条是否开始
|
||||||
|
const loadingFinishing = ref(false); // 加载条是否完成
|
||||||
|
const loadingErroring = ref(false); // 加载条是否报错
|
||||||
|
async function init() {
|
||||||
|
showLoadingBar.value = false;
|
||||||
|
loadingFinishing.value = false;
|
||||||
|
loadingErroring.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start(from = 0, to = 80, status: 'starting' | 'error' = 'starting') {
|
||||||
|
// 加载条开始加载的回调函数
|
||||||
|
loadingStarted.value = true;
|
||||||
|
await init();
|
||||||
|
if (loadingFinishing.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showLoadingBar.value = true;
|
||||||
|
await nextTick();
|
||||||
|
if (!loadingBarRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingBarRef.value.style.transition = 'none'; // 禁用过渡
|
||||||
|
loadingBarRef.value.style.maxWidth = `${from}%`;
|
||||||
|
void loadingBarRef.value.offsetWidth; // 触发浏览器回流(重排)
|
||||||
|
loadingBarRef.value.className = `loading-bar loading-bar-${status}`;
|
||||||
|
loadingBarRef.value.style.transition = '';
|
||||||
|
loadingBarRef.value.style.maxWidth = `${to}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finish() {
|
||||||
|
// 加载条结束加载的回调函数
|
||||||
|
if (loadingFinishing.value || loadingErroring.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loadingStarted.value) {
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
loadingFinishing.value = true;
|
||||||
|
if (!loadingBarRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingBarRef.value.className = 'loading-bar loading-bar-finishing';
|
||||||
|
loadingBarRef.value.style.maxWidth = '100%';
|
||||||
|
void loadingBarRef.value.offsetWidth; // 触发浏览器回流(重排)
|
||||||
|
showLoadingBar.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function error() {
|
||||||
|
// 加载条出现错误的回调函数
|
||||||
|
if (loadingFinishing.value || loadingErroring.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!showLoadingBar.value) {
|
||||||
|
void start(100, 100, 'error').then(() => {
|
||||||
|
loadingErroring.value = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadingErroring.value = true;
|
||||||
|
if (!loadingBarRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingBarRef.value.className = 'loading-bar loading-bar-error';
|
||||||
|
loadingBarRef.value.style.maxWidth = '100%';
|
||||||
|
void loadingBarRef.value.offsetWidth;
|
||||||
|
showLoadingBar.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAfterEnter() {
|
||||||
|
if (loadingErroring.value) {
|
||||||
|
showLoadingBar.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAfterLeave() {
|
||||||
|
await init();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
start,
|
||||||
|
finish,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Teleport :disabled="!to" :to="to">
|
||||||
|
<Transition name="fade-in" @after-enter="onAfterEnter" @after-leave="onAfterLeave">
|
||||||
|
<div v-show="showLoadingBar" class="m-loading-bar-container" :class="containerClass" :style="containerStyle">
|
||||||
|
<div
|
||||||
|
ref="loadingBarRef"
|
||||||
|
class="loading-bar"
|
||||||
|
:style="`--loading-bar-size: ${loadingBarSize}px; --color-loading: ${colorLoading}; --color-finish: ${colorFinish}; --color-error: ${colorError}; max-width: 100%;`"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.fade-in-enter-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-leave-active {
|
||||||
|
transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-enter-from,
|
||||||
|
.fade-in-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-loading-bar-container {
|
||||||
|
z-index: 9999;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: var(--loading-bar-size);
|
||||||
|
|
||||||
|
.loading-bar {
|
||||||
|
width: 100%;
|
||||||
|
transition: max-width 4s linear,
|
||||||
|
background 0.2s linear;
|
||||||
|
height: var(--loading-bar-size);
|
||||||
|
border-radius: var(--loading-bar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-bar-starting {
|
||||||
|
background: var(--color-loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-bar-finishing {
|
||||||
|
background: var(--color-finish);
|
||||||
|
transition: max-width 0.2s linear,
|
||||||
|
background 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-bar-error {
|
||||||
|
background: var(--color-error);
|
||||||
|
transition: max-width 0.2s linear,
|
||||||
|
background 0.2s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
429
src/components/MyUI/Message/Message.vue
Normal file
429
src/components/MyUI/Message/Message.vue
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties, VNode} from 'vue';
|
||||||
|
import {computed, ref, watch} from 'vue';
|
||||||
|
import {cancelRaf, rafTimeout} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
content?: string // 提示内容
|
||||||
|
duration?: number // 自动关闭的延时,单位 ms,设置 null 时,不自动关闭
|
||||||
|
top?: string | number // 消息距离顶部的位置,单位 px
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
content: undefined,
|
||||||
|
duration: 3000,
|
||||||
|
top: 30
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
content?: string // 提示内容
|
||||||
|
icon?: VNode // 自定义图标
|
||||||
|
duration?: number | null // 自动关闭的延时时长,单位 ms;设置 null 时,不自动关闭
|
||||||
|
top?: string | number // 消息距离顶部的位置,单位 px
|
||||||
|
class?: string // 自定义类名
|
||||||
|
style?: CSSProperties // 自定义样式
|
||||||
|
onClick?: () => void // 点击 message 时的回调函数
|
||||||
|
onClose?: () => void // 关闭时的回调函数
|
||||||
|
mode?: 'open' | 'info' | 'success' | 'error' | 'warning' | 'loading' // 类型
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTimer = ref();
|
||||||
|
const showMessage = ref<boolean[]>([]);
|
||||||
|
const hideTimers = ref<any[]>([]);
|
||||||
|
const messageContent = ref<Message[]>([]);
|
||||||
|
const closeDuration = ref<number | null>(null); // 自动关闭延时
|
||||||
|
const emits = defineEmits(['click', 'close']);
|
||||||
|
const messageTop = ref<string>();
|
||||||
|
const clear = computed(() => {
|
||||||
|
// 所有提示是否已经全部变为false
|
||||||
|
return showMessage.value.every((show) => !show);
|
||||||
|
});
|
||||||
|
watch(clear, (to, from) => {
|
||||||
|
// 所有提示都消失后重置
|
||||||
|
if (!from && to) {
|
||||||
|
resetTimer.value = rafTimeout(() => {
|
||||||
|
messageContent.value.splice(0);
|
||||||
|
showMessage.value.splice(0);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onEnter(index: number) {
|
||||||
|
if (hideTimers.value[index]) {
|
||||||
|
cancelRaf(hideTimers.value[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeave(index: number) {
|
||||||
|
hideMessage(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(e: Event, index: number) {
|
||||||
|
if (messageContent.value[index].onClick) {
|
||||||
|
messageContent.value[index].onClick();
|
||||||
|
}
|
||||||
|
emits('click', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideMessage(index: number) {
|
||||||
|
if (closeDuration.value !== null) {
|
||||||
|
hideTimers.value[index] = rafTimeout(() => {
|
||||||
|
showMessage.value[index] = false;
|
||||||
|
if (messageContent.value[index].onClose) {
|
||||||
|
messageContent.value[index].onClose();
|
||||||
|
}
|
||||||
|
emits('close');
|
||||||
|
}, closeDuration.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (resetTimer.value) {
|
||||||
|
cancelRaf(resetTimer.value);
|
||||||
|
}
|
||||||
|
const index = messageContent.value.length - 1;
|
||||||
|
const last = messageContent.value[index];
|
||||||
|
if (last.top !== undefined) {
|
||||||
|
messageTop.value = typeof last.top === 'number' ? `${last.top}px` : last.top;
|
||||||
|
} else {
|
||||||
|
messageTop.value = typeof props.top === 'number' ? `${props.top}px` : props.top;
|
||||||
|
}
|
||||||
|
showMessage.value[index] = true;
|
||||||
|
if (last.duration !== null) {
|
||||||
|
closeDuration.value = last.duration || props.duration;
|
||||||
|
hideMessage(index);
|
||||||
|
} else {
|
||||||
|
closeDuration.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(message: string | Message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
messageContent.value.push({
|
||||||
|
content: message,
|
||||||
|
mode: 'open'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messageContent.value.push({
|
||||||
|
...message,
|
||||||
|
mode: 'open'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(message: string | Message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
messageContent.value.push({
|
||||||
|
content: message,
|
||||||
|
mode: 'info'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messageContent.value.push({
|
||||||
|
...message,
|
||||||
|
mode: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(message: string | Message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
messageContent.value.push({
|
||||||
|
content: message,
|
||||||
|
mode: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messageContent.value.push({
|
||||||
|
...message,
|
||||||
|
mode: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message: string | Message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
messageContent.value.push({
|
||||||
|
content: message,
|
||||||
|
mode: 'error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messageContent.value.push({
|
||||||
|
...message,
|
||||||
|
mode: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning(message: string | Message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
messageContent.value.push({
|
||||||
|
content: message,
|
||||||
|
mode: 'warning'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messageContent.value.push({
|
||||||
|
...message,
|
||||||
|
mode: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loading(message: string | Message) {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
messageContent.value.push({
|
||||||
|
content: message,
|
||||||
|
mode: 'loading'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messageContent.value.push({
|
||||||
|
...message,
|
||||||
|
mode: 'loading'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
info,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
loading
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-message-wrap" :style="`top: ${messageTop};`">
|
||||||
|
<TransitionGroup name="slide-fade">
|
||||||
|
<div
|
||||||
|
v-show="showMessage[index]"
|
||||||
|
class="m-message"
|
||||||
|
:class="message.class"
|
||||||
|
:style="message.style"
|
||||||
|
v-for="(message, index) in messageContent"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="m-message-content"
|
||||||
|
:class="`icon-${message.mode}`"
|
||||||
|
@mouseenter="onEnter(index)"
|
||||||
|
@mouseleave="onLeave(index)"
|
||||||
|
@click="onClick($event, index)"
|
||||||
|
>
|
||||||
|
<component v-if="message.icon" :is="message.icon" class="icon-svg"/>
|
||||||
|
<svg
|
||||||
|
v-else-if="message.mode === 'info'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="info-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="message.mode === 'success'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="check-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="message.mode === 'error'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="message.mode === 'warning'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="exclamation-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="message.mode === 'loading'"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
class="icon-svg circle"
|
||||||
|
viewBox="0 0 50 50"
|
||||||
|
>
|
||||||
|
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
|
||||||
|
</svg>
|
||||||
|
<div class="message-content">
|
||||||
|
{{ message.content || content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
// 滑动渐变过渡效果
|
||||||
|
.slide-fade-move,
|
||||||
|
.slide-fade-enter-active,
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from,
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
transform: translateY(-16px);
|
||||||
|
-ms-transform: translateY(-16px); /* IE 9 */
|
||||||
|
-webkit-transform: translateY(-16px); /* Safari and Chrome */
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-message-wrap {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999; // 突出显示该层级
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
pointer-events: none; // 保证整个message区域不遮挡背后元素响应鼠标事件
|
||||||
|
.m-message {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-message-content {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
pointer-events: auto; // 保证内容区域部分可以正常响应鼠标事件
|
||||||
|
:deep(.icon-svg) {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
display: inline-block;
|
||||||
|
stroke: currentColor;
|
||||||
|
animation: loading-rotate 2s linear infinite;
|
||||||
|
@keyframes loading-rotate {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
animation: loading-dash 1.5s ease-in-out infinite;
|
||||||
|
@keyframes loading-dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -40px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-open {
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-info,
|
||||||
|
.icon-loading {
|
||||||
|
:deep(svg) {
|
||||||
|
color: #909399;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-success {
|
||||||
|
:deep(svg) {
|
||||||
|
color: #52c41a;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-warning {
|
||||||
|
:deep(svg) {
|
||||||
|
color: #faad14;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-error {
|
||||||
|
:deep(svg) {
|
||||||
|
color: #ff4d4f;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
648
src/components/MyUI/Modal/Modal.vue
Normal file
648
src/components/MyUI/Modal/Modal.vue
Normal file
@@ -0,0 +1,648 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties, Slot, VNode} from 'vue';
|
||||||
|
import {computed, nextTick, onMounted, onUnmounted, ref, watch, watchEffect} from 'vue';
|
||||||
|
import Button from '../Button/Button.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 模态框宽度,单位 px
|
||||||
|
icon?: VNode | Slot // 自定义图标
|
||||||
|
title?: string // 模态框标题 string | slot
|
||||||
|
titleStyle?: CSSProperties // 自定义标题样式
|
||||||
|
content?: string // 模态框内容 string | slot
|
||||||
|
contentStyle?: CSSProperties // 自定义内容样式
|
||||||
|
bodyClass?: string // 自定义 body 类名
|
||||||
|
bodyStyle?: CSSProperties // 自定义 body 样式
|
||||||
|
cancelText?: string // 取消按钮文字
|
||||||
|
cancelProps?: object // 取消按钮 props 配置,参考 Button 组件 Props
|
||||||
|
okText?: string // 确认按钮文字
|
||||||
|
okType?: 'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link' // 确认按钮类型
|
||||||
|
okProps?: object // 确认按钮 props 配置,优先级高于 okType,参考 Button 组件 Props
|
||||||
|
noticeText?: string // 通知按钮文字
|
||||||
|
noticeProps?: object // 通知按钮 props 配置,参考 Button 组件 Props
|
||||||
|
centered?: boolean // 是否水平垂直居中,否则固定高度水平居中
|
||||||
|
top?: string | number // 固定高度水平居中时,距顶部高度,仅当 center: false 时生效,单位 px
|
||||||
|
transformOrigin?: 'mouse' | 'center' // 模态框动画出现的位置
|
||||||
|
confirmLoading?: boolean // 确认按钮 loading
|
||||||
|
blockScroll?: boolean // 是否在打开模态框时禁用背景滚动
|
||||||
|
keyboard?: boolean // 是否支持键盘 esc 关闭
|
||||||
|
maskClosable?: boolean // 点击蒙层是否允许关闭
|
||||||
|
maskStyle?: CSSProperties // 自定义蒙层样式
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 420,
|
||||||
|
icon: undefined,
|
||||||
|
title: undefined,
|
||||||
|
titleStyle: () => ({}),
|
||||||
|
content: undefined,
|
||||||
|
contentStyle: () => ({}),
|
||||||
|
bodyClass: undefined,
|
||||||
|
bodyStyle: () => ({}),
|
||||||
|
cancelText: '取消',
|
||||||
|
cancelProps: () => ({}),
|
||||||
|
okText: '确定',
|
||||||
|
okType: 'primary',
|
||||||
|
okProps: () => ({}),
|
||||||
|
noticeText: '知道了',
|
||||||
|
noticeProps: () => ({}),
|
||||||
|
centered: false,
|
||||||
|
top: 100,
|
||||||
|
transformOrigin: 'mouse',
|
||||||
|
confirmLoading: false,
|
||||||
|
blockScroll: true,
|
||||||
|
keyboard: true,
|
||||||
|
maskClosable: true,
|
||||||
|
maskStyle: () => ({})
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Modal {
|
||||||
|
width?: string | number // 模态框宽度,单位 px
|
||||||
|
icon?: VNode // 自定义图标
|
||||||
|
title?: string // 模态框标题
|
||||||
|
titleStyle?: CSSProperties // 自定义标题样式
|
||||||
|
content?: string // 模态框内容
|
||||||
|
contentStyle?: CSSProperties // 自定义内容样式
|
||||||
|
bodyClass?: string // 自定义 body 类名
|
||||||
|
bodyStyle?: CSSProperties // 自定义 body 样式
|
||||||
|
cancelText?: string // 取消按钮文字
|
||||||
|
cancelProps?: object // 取消按钮 props 配置,参考 Button 组件 Props
|
||||||
|
okText?: string // 确认按钮文字
|
||||||
|
okType?: 'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link' // 确认按钮类型
|
||||||
|
okProps?: object // 确认按钮 props 配置,优先级高于 okType,参考 Button 组件 Props
|
||||||
|
noticeText?: string // 通知按钮文字
|
||||||
|
noticeProps?: object // 通知按钮 props 配置,参考 Button 组件 Props
|
||||||
|
centered?: boolean // 是否水平垂直居中,否则固定高度水平居中
|
||||||
|
top?: string | number // 固定高度水平居中时,距顶部高度,仅当 center: false 时生效,单位 px
|
||||||
|
transformOrigin?: 'mouse' | 'center' // 模态框动画出现的位置
|
||||||
|
blockScroll?: boolean // 是否在打开模态框时禁用背景滚动
|
||||||
|
keyboard?: boolean // 是否支持键盘 esc 关闭
|
||||||
|
maskClosable?: boolean // 点击蒙层是否允许关闭
|
||||||
|
maskStyle?: CSSProperties // 自定义蒙层样式
|
||||||
|
onKnow?: any // 点击知道了按钮的回调
|
||||||
|
onOk?: any // 点击确认按钮的回调
|
||||||
|
onCancel?: any // 点击遮罩层或取消按钮的回调
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalWrapRef = ref(); // modal DOM 引用
|
||||||
|
const mousePosition = ref<{ x: number; y: number } | null>(null); // 鼠标点击位置
|
||||||
|
const modalOpen = ref<boolean>(false);
|
||||||
|
const showModalWrap = ref<boolean>(false);
|
||||||
|
const confirmBtnLoading = ref<boolean>(false);
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
const transformOrigin = ref<string>('50% 50%');
|
||||||
|
const modalData = ref<Modal>();
|
||||||
|
const modalMode = ref(); // 弹窗类型:'info' 'success' 'error' 'warning' 'confirm' 'erase'
|
||||||
|
const emits = defineEmits(['update:open', 'cancel', 'ok', 'know']);
|
||||||
|
const modalWidth = computed(() => {
|
||||||
|
const width = getComputedValue('width');
|
||||||
|
return typeof width === 'number' ? `${width}px` : width;
|
||||||
|
});
|
||||||
|
const modalTop = computed(() => {
|
||||||
|
const top = getComputedValue('top');
|
||||||
|
return typeof top === 'number' ? `${top}px` : top;
|
||||||
|
});
|
||||||
|
const modalCentered = computed(() => {
|
||||||
|
return getComputedValue('centered');
|
||||||
|
});
|
||||||
|
const modalStyle = computed(() => {
|
||||||
|
if (modalCentered.value) {
|
||||||
|
return {
|
||||||
|
width: modalWidth.value,
|
||||||
|
transformOrigin: transformOrigin.value
|
||||||
|
} as CSSProperties;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
width: modalWidth.value,
|
||||||
|
top: modalTop.value,
|
||||||
|
transformOrigin: transformOrigin.value
|
||||||
|
} as CSSProperties;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const modalTitleStyle = computed(() => {
|
||||||
|
return getComputedValue('titleStyle') as CSSProperties;
|
||||||
|
});
|
||||||
|
const modalContentStyle = computed(() => {
|
||||||
|
return getComputedValue('contentStyle') as CSSProperties;
|
||||||
|
});
|
||||||
|
const modalBodyClass = computed(() => {
|
||||||
|
return getComputedValue('bodyClass');
|
||||||
|
});
|
||||||
|
const modalBodyStyle = computed(() => {
|
||||||
|
return getComputedValue('bodyStyle') as CSSProperties;
|
||||||
|
});
|
||||||
|
const modalMaskStyle = computed(() => {
|
||||||
|
return getComputedValue('maskStyle') as CSSProperties;
|
||||||
|
});
|
||||||
|
const modalIcon = computed(() => {
|
||||||
|
return getComputedValue('icon');
|
||||||
|
});
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
return getComputedValue('title');
|
||||||
|
});
|
||||||
|
const modalContent = computed(() => {
|
||||||
|
return getComputedValue('content');
|
||||||
|
});
|
||||||
|
const modalCancelProps: object = computed(() => {
|
||||||
|
return getComputedValue('cancelProps');
|
||||||
|
});
|
||||||
|
const modalCancelText = computed(() => {
|
||||||
|
return getComputedValue('cancelText');
|
||||||
|
});
|
||||||
|
const modalOkType = computed(() => {
|
||||||
|
return getComputedValue('okType') as 'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link';
|
||||||
|
});
|
||||||
|
const modalOkProps: object = computed(() => {
|
||||||
|
return getComputedValue('okProps');
|
||||||
|
});
|
||||||
|
const modalOkText = computed(() => {
|
||||||
|
return getComputedValue('okText');
|
||||||
|
});
|
||||||
|
const modalNoticeProps: object = computed(() => {
|
||||||
|
return getComputedValue('noticeProps');
|
||||||
|
});
|
||||||
|
const modalNoticeText = computed(() => {
|
||||||
|
return getComputedValue('noticeText');
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
modalOpen,
|
||||||
|
async (to) => {
|
||||||
|
const blockScroll = getComputedValue('blockScroll');
|
||||||
|
if (to) {
|
||||||
|
await nextTick();
|
||||||
|
modalWrapRef.value.focus();
|
||||||
|
if (blockScroll) {
|
||||||
|
// 锁定滚动
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
document.body.style.overflowY = 'hidden';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (blockScroll) {
|
||||||
|
// 解锁滚动
|
||||||
|
document.documentElement.style.removeProperty('overflow-y');
|
||||||
|
document.body.style.removeProperty('overflow-y');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watchEffect(() => {
|
||||||
|
confirmBtnLoading.value = props.confirmLoading;
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', getClickPosition, true); // 事件在捕获阶段执行
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', getClickPosition, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getClickPosition(e: MouseEvent) {
|
||||||
|
if (!modalOpen.value) {
|
||||||
|
mousePosition.value = {
|
||||||
|
x: e.clientX, // 相对于浏览器视口左上角的 X 坐标,不页面滚动而改变
|
||||||
|
y: e.clientY // 相对于浏览器视口左上角的 Y 坐标,不页面滚动而改变
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onBeforeEnter(el: Element) {
|
||||||
|
showModalWrap.value = true;
|
||||||
|
await nextTick();
|
||||||
|
const transOrigin = getComputedValue('transformOrigin');
|
||||||
|
if (transOrigin === 'mouse' && mousePosition.value) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
transformOrigin.value = `${mousePosition.value.x - rect.left}px ${mousePosition.value.y - rect.top}px`;
|
||||||
|
} else {
|
||||||
|
transformOrigin.value = '50% 50%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBeforeLeave(el: Element) {
|
||||||
|
const transOrigin = getComputedValue('transformOrigin');
|
||||||
|
if (transOrigin === 'mouse' && mousePosition.value) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
transformOrigin.value = `${mousePosition.value.x - rect.left}px ${mousePosition.value.y - rect.top}px`;
|
||||||
|
} else {
|
||||||
|
transformOrigin.value = '50% 50%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAfterLeave() {
|
||||||
|
showModalWrap.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getComputedValue(key: keyof Props) {
|
||||||
|
let computedValue = props[key as keyof Props];
|
||||||
|
if (modalData.value?.[key as keyof Modal] !== undefined) {
|
||||||
|
computedValue = modalData.value[key as keyof Modal];
|
||||||
|
}
|
||||||
|
return computedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(data: Modal) {
|
||||||
|
modalMode.value = 'info';
|
||||||
|
modalData.value = data;
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(data: Modal) {
|
||||||
|
modalMode.value = 'success';
|
||||||
|
modalData.value = data;
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(data: Modal) {
|
||||||
|
modalMode.value = 'error';
|
||||||
|
modalData.value = data;
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning(data: Modal) {
|
||||||
|
modalMode.value = 'warning';
|
||||||
|
modalData.value = data;
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm(data: Modal) {
|
||||||
|
modalMode.value = 'confirm';
|
||||||
|
modalData.value = data;
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function erase(data: Modal) {
|
||||||
|
modalMode.value = 'erase';
|
||||||
|
modalData.value = data;
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
modalOpen.value = true;
|
||||||
|
emits('update:open', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
if (modalData.value?.onCancel) {
|
||||||
|
modalData.value.onCancel();
|
||||||
|
}
|
||||||
|
modalOpen.value = false;
|
||||||
|
emits('cancel');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onOK() {
|
||||||
|
if (modalData.value?.onOk) {
|
||||||
|
confirmBtnLoading.value = true;
|
||||||
|
await modalData.value.onOk();
|
||||||
|
confirmBtnLoading.value = false;
|
||||||
|
}
|
||||||
|
modalOpen.value = false;
|
||||||
|
emits('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKnow() {
|
||||||
|
if (modalData.value?.onKnow) {
|
||||||
|
modalData.value.onKnow();
|
||||||
|
}
|
||||||
|
modalOpen.value = false;
|
||||||
|
emits('know');
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
info,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
confirm,
|
||||||
|
erase
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-modal-root">
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-show="modalOpen" class="m-modal-mask" :style="modalMaskStyle"></div>
|
||||||
|
</Transition>
|
||||||
|
<div
|
||||||
|
v-show="showModalWrap"
|
||||||
|
tabindex="-1"
|
||||||
|
ref="modalWrapRef"
|
||||||
|
class="m-modal-wrap"
|
||||||
|
:class="{ 'flex-centered': modalCentered }"
|
||||||
|
@click.self="getComputedValue('maskClosable') ? onCancel() : () => false"
|
||||||
|
@keydown.esc="getComputedValue('keyboard') ? onCancel() : () => false"
|
||||||
|
>
|
||||||
|
<Transition
|
||||||
|
name="zoom"
|
||||||
|
enter-from-class="zoom-enter"
|
||||||
|
enter-active-class="zoom-enter"
|
||||||
|
enter-to-class="zoom-enter zoom-enter-active"
|
||||||
|
leave-from-class="zoom-leave"
|
||||||
|
leave-active-class="zoom-leave zoom-leave-active"
|
||||||
|
leave-to-class="zoom-leave zoom-leave-active"
|
||||||
|
@before-enter="onBeforeEnter"
|
||||||
|
@before-leave="onBeforeLeave"
|
||||||
|
@after-leave="onAfterLeave"
|
||||||
|
>
|
||||||
|
<div v-show="modalOpen" class="m-modal" :style="modalStyle">
|
||||||
|
<div class="m-modal-body-wrap" :class="modalBodyClass" :style="modalBodyStyle">
|
||||||
|
<div class="m-modal-body">
|
||||||
|
<div
|
||||||
|
class="modal-header"
|
||||||
|
:class="{
|
||||||
|
[`icon-${modalMode}`]: ['info', 'success', 'error', 'warning', 'confirm', 'erase'].includes(modalMode)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot name="icon">
|
||||||
|
<component v-if="modalIcon" :is="modalIcon" class="icon-svg"/>
|
||||||
|
<svg
|
||||||
|
v-else-if="modalMode === 'confirm' || modalMode === 'erase'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="exclamation-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M464 688a48 48 0 1096 0 48 48 0 10-96 0zm24-112h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="modalMode === 'info'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="info-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="modalMode === 'success'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="check-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="modalMode === 'error'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="modalMode === 'warning'"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="exclamation-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
<div class="modal-title" :style="modalTitleStyle">
|
||||||
|
<slot name="title">{{ modalTitle }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-content" :style="modalContentStyle">
|
||||||
|
<slot>{{ modalContent }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-btns">
|
||||||
|
<template v-if="['confirm', 'erase'].includes(modalMode)">
|
||||||
|
<Button class="mr8" @click="onCancel" v-bind="modalCancelProps">
|
||||||
|
{{ modalCancelText }}
|
||||||
|
</Button>
|
||||||
|
<Button :type="modalOkType" :loading="confirmBtnLoading" @click="onOK" v-bind="modalOkProps">
|
||||||
|
{{ modalOkText }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<Button
|
||||||
|
v-if="['info', 'success', 'error', 'warning'].includes(modalMode)"
|
||||||
|
type="primary"
|
||||||
|
:loading="confirmBtnLoading"
|
||||||
|
@click="onKnow"
|
||||||
|
v-bind="modalNoticeProps"
|
||||||
|
>
|
||||||
|
{{ modalNoticeText }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-enter {
|
||||||
|
transform: none;
|
||||||
|
opacity: 0;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-enter-active {
|
||||||
|
animation-name: zoomIn;
|
||||||
|
animation-play-state: running;
|
||||||
|
@keyframes zoomIn {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-leave {
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-play-state: paused;
|
||||||
|
animation-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-leave-active {
|
||||||
|
animation-name: zoomOut;
|
||||||
|
animation-play-state: running;
|
||||||
|
@keyframes zoomOut {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-modal-wrap {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
overflow: auto;
|
||||||
|
outline: 0;
|
||||||
|
z-index: 1010;
|
||||||
|
|
||||||
|
.m-modal {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.m-modal-body-wrap {
|
||||||
|
position: relative;
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: auto;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.m-modal-body {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(.icon-svg) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-top: 1px;
|
||||||
|
font-size: 22px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-confirm,
|
||||||
|
.icon-erase {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-info {
|
||||||
|
color: @themeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-success {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-warning {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
margin-left: 34px;
|
||||||
|
max-width: calc(100% - 34px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btns {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.mr8 {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-centered {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.m-modal {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
454
src/components/MyUI/Notification/Notification.vue
Normal file
454
src/components/MyUI/Notification/Notification.vue
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties, VNode} from 'vue';
|
||||||
|
import {computed, nextTick, ref, watch, watchEffect} from 'vue';
|
||||||
|
import {cancelRaf, rafTimeout} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string // 通知提醒标题,优先级低于 Notification 中的 title
|
||||||
|
description?: string // 通知提醒内容,优先级低于 Notification 中的 description
|
||||||
|
duration?: number | null // 自动关闭的延时时长,单位 ms;设置 null 时,不自动关闭,优先级低于 Notification 中的 duration
|
||||||
|
top?: number // 消息从顶部弹出时,距离顶部的位置,单位 px
|
||||||
|
bottom?: number // 消息从底部弹出时,距离底部的位置,单位 px
|
||||||
|
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' // 消息弹出位置,优先级低于 Notification 中的 placement
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: undefined,
|
||||||
|
description: undefined,
|
||||||
|
duration: 4500,
|
||||||
|
top: 24,
|
||||||
|
bottom: 24,
|
||||||
|
placement: 'topRight'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
title?: string // 通知提醒标题
|
||||||
|
description?: string // 通知提醒内容
|
||||||
|
icon?: VNode // 自定义图标
|
||||||
|
class?: string // 自定义类名
|
||||||
|
style?: CSSProperties // 自定义样式
|
||||||
|
duration?: number | null // 自动关闭的延时时长,单位 ms;设置 null 时,不自动关闭
|
||||||
|
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' // 通知提醒弹出位置
|
||||||
|
onClose?: any // 关闭时的回调函数
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTimer = ref();
|
||||||
|
const hideIndex = ref<number[]>([]);
|
||||||
|
const hideTimers = ref<any[]>([]);
|
||||||
|
const notificationData = ref<any[]>([]);
|
||||||
|
const closeDuration = ref<number | null>(null); // 自动关闭延时
|
||||||
|
const notificationPlace = ref(); // 弹出位置
|
||||||
|
const notificationRef = ref(); // notificationData 数组的 DOM 引用
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
const topStyle = computed(() => {
|
||||||
|
if (['topRight', 'topLeft'].includes(notificationPlace.value)) {
|
||||||
|
return {
|
||||||
|
top: `${props.top}px`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const bottomStyle = computed(() => {
|
||||||
|
if (['bottomRight', 'bottomLeft'].includes(notificationPlace.value)) {
|
||||||
|
return {
|
||||||
|
bottom: `${props.bottom}px`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const clear = computed(() => {
|
||||||
|
// 所有提示是否已经全部变为 false
|
||||||
|
return hideIndex.value.length === notificationData.value.length;
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
clear,
|
||||||
|
(to, from) => {
|
||||||
|
// 所有提示都消失后重置
|
||||||
|
if (!from && to) {
|
||||||
|
resetTimer.value = rafTimeout(() => {
|
||||||
|
hideIndex.value.splice(0);
|
||||||
|
notificationData.value.splice(0);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flush: 'post'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watchEffect(() => {
|
||||||
|
notificationPlace.value = props.placement;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onEnter(index: number) {
|
||||||
|
stopAutoClose(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeave(index: number) {
|
||||||
|
if (!hideIndex.value.includes(index)) {
|
||||||
|
autoClose(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAutoClose(index: number) {
|
||||||
|
cancelRaf(hideTimers.value[index]);
|
||||||
|
hideTimers.value[index] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoClose(index: number) {
|
||||||
|
if (closeDuration.value !== null) {
|
||||||
|
hideTimers.value[index] = rafTimeout(() => {
|
||||||
|
onClose(index);
|
||||||
|
}, closeDuration.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onClose(index: number) {
|
||||||
|
notificationRef.value[index].style.maxHeight = notificationRef.value[index].offsetHeight + 'px';
|
||||||
|
await nextTick();
|
||||||
|
hideIndex.value.push(index);
|
||||||
|
notificationData.value[index].onClose();
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
cancelRaf(resetTimer.value);
|
||||||
|
hideTimers.value.push(null);
|
||||||
|
const index = notificationData.value.length - 1;
|
||||||
|
const last = notificationData.value[index];
|
||||||
|
if (last.placement) {
|
||||||
|
notificationPlace.value = last.placement;
|
||||||
|
}
|
||||||
|
if (last.duration !== null) {
|
||||||
|
closeDuration.value = last.duration || props.duration;
|
||||||
|
autoClose(index);
|
||||||
|
} else {
|
||||||
|
closeDuration.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(notification: Notification) {
|
||||||
|
notificationData.value.push({
|
||||||
|
...notification,
|
||||||
|
mode: 'open'
|
||||||
|
});
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(notification: Notification) {
|
||||||
|
notificationData.value.push({
|
||||||
|
...notification,
|
||||||
|
mode: 'info'
|
||||||
|
});
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(notification: Notification) {
|
||||||
|
notificationData.value.push({
|
||||||
|
...notification,
|
||||||
|
mode: 'success'
|
||||||
|
});
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(notification: Notification) {
|
||||||
|
notificationData.value.push({
|
||||||
|
...notification,
|
||||||
|
mode: 'error'
|
||||||
|
});
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning(notification: Notification) {
|
||||||
|
notificationData.value.push({
|
||||||
|
...notification,
|
||||||
|
mode: 'warning'
|
||||||
|
});
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
info,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-notification-wrap"
|
||||||
|
:class="`notification-${notificationPlace}`"
|
||||||
|
:style="{ ...topStyle, ...bottomStyle }"
|
||||||
|
>
|
||||||
|
<TransitionGroup :name="['topRight', 'bottomRight'].includes(notificationPlace) ? 'right' : 'left'">
|
||||||
|
<div
|
||||||
|
v-show="!hideIndex.includes(index)"
|
||||||
|
ref="notificationRef"
|
||||||
|
class="m-notification-content"
|
||||||
|
:class="[`icon-${notification.mode}`, notification.class]"
|
||||||
|
:style="notification.style"
|
||||||
|
v-for="(notification, index) in notificationData"
|
||||||
|
:key="index"
|
||||||
|
@mouseenter="onEnter(index)"
|
||||||
|
@mouseleave="onLeave(index)"
|
||||||
|
>
|
||||||
|
<component v-if="notification.icon" :is="notification.icon" class="icon-svg"/>
|
||||||
|
<svg
|
||||||
|
v-else-if="notification.mode === 'info'"
|
||||||
|
class="icon-svg"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="info-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M464 336a48 48 0 1 0 96 0 48 48 0 1 0-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="notification.mode === 'success'"
|
||||||
|
class="icon-svg"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="check-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0 0 51.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="notification.mode === 'warning'"
|
||||||
|
class="icon-svg"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="exclamation-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M464 688a48 48 0 1 0 96 0 48 48 0 1 0-96 0zm24-112h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="notification.mode === 'error'"
|
||||||
|
class="icon-svg"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="close-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M685.4 354.8c0-4.4-3.6-8-8-8l-66 .3L512 465.6l-99.3-118.4-66.1-.3c-4.4 0-8 3.5-8 8 0 1.9.7 3.7 1.9 5.2l130.1 155L340.5 670a8.32 8.32 0 0 0-1.9 5.2c0 4.4 3.6 8 8 8l66.1-.3L512 564.4l99.3 118.4 66 .3c4.4 0 8-3.5 8-8 0-1.9-.7-3.7-1.9-5.2L553.5 515l130.1-155c1.2-1.4 1.8-3.3 1.8-5.2z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M512 65C264.6 65 64 265.6 64 513s200.6 448 448 448 448-200.6 448-448S759.4 65 512 65zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-title">{{ notification.title || title }}</div>
|
||||||
|
<div class="notification-description">{{ notification.description || description }}</div>
|
||||||
|
</div>
|
||||||
|
<a tabindex="0" class="notification-close" @click="onClose(index)">
|
||||||
|
<svg
|
||||||
|
class="close-svg"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="close"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.right-move, // 对移动中的元素应用的过渡
|
||||||
|
.right-enter-active,
|
||||||
|
.right-leave-active,
|
||||||
|
.left-move,
|
||||||
|
.left-enter-active,
|
||||||
|
.left-leave-active {
|
||||||
|
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-leave-to,
|
||||||
|
.left-leave-to {
|
||||||
|
max-height: 0 !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-enter-from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保将离开的元素从布局流中删除
|
||||||
|
以便能够正确地计算移动的动画。 */
|
||||||
|
.right-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-enter-from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-notification-wrap {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999; // 突出显示该层级
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
margin-right: 24px;
|
||||||
|
|
||||||
|
.m-notification-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 384px;
|
||||||
|
max-width: calc(100vw - 48px);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 20px 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
:deep(.icon-svg) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 24px;
|
||||||
|
fill: currentColor;
|
||||||
|
margin-right: 12px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
padding-right: 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-description {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
outline: none;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
|
||||||
|
.close-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
fill: currentColor;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.close-svg {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-info {
|
||||||
|
:deep(.icon-svg) {
|
||||||
|
color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-success {
|
||||||
|
:deep(.icon-svg) {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-warning {
|
||||||
|
:deep(.icon-svg) {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-error {
|
||||||
|
:deep(.icon-svg) {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-topRight,
|
||||||
|
.notification-bottomRight {
|
||||||
|
margin-right: 24px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-topLeft,
|
||||||
|
.notification-bottomLeft {
|
||||||
|
margin-left: 24px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
99
src/components/MyUI/NumberAnimation/NumberAnimation.vue
Normal file
99
src/components/MyUI/NumberAnimation/NumberAnimation.vue
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed, onMounted, ref, watch, watchEffect} from 'vue';
|
||||||
|
import {formatNumber} from '../Utils';
|
||||||
|
import {TransitionPresets, useTransition} from '@vueuse/core';
|
||||||
|
|
||||||
|
enum TransitionFunc {
|
||||||
|
linear = 'linear',
|
||||||
|
easeOutSine = 'easeOutSine',
|
||||||
|
easeInOutSine = 'easeInOutSine',
|
||||||
|
easeInQuad = 'easeInQuad',
|
||||||
|
easeOutQuad = 'easeOutQuad',
|
||||||
|
easeInOutQuad = 'easeInOutQuad',
|
||||||
|
easeInCubic = 'easeInCubic',
|
||||||
|
easeOutCubic = 'easeOutCubic',
|
||||||
|
easeInOutCubic = 'easeInOutCubic',
|
||||||
|
easeInQuart = 'easeInQuart',
|
||||||
|
easeOutQuart = 'easeOutQuart',
|
||||||
|
easeInOutQuart = 'easeInOutQuart',
|
||||||
|
easeInQuint = 'easeInQuint',
|
||||||
|
easeOutQuint = 'easeOutQuint',
|
||||||
|
easeInOutQuint = 'easeInOutQuint',
|
||||||
|
easeInExpo = 'easeInExpo',
|
||||||
|
easeOutExpo = 'easeOutExpo',
|
||||||
|
easeInOutExpo = 'easeInOutExpo',
|
||||||
|
easeInCirc = 'easeInCirc',
|
||||||
|
easeOutCirc = 'easeOutCirc',
|
||||||
|
easeInOutCirc = 'easeInOutCirc',
|
||||||
|
easeInBack = 'easeInBack',
|
||||||
|
easeOutBack = 'easeOutBack',
|
||||||
|
easeInOutBack = 'easeInOutBack'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
from?: number // 数值动画起始数值
|
||||||
|
to?: number // 数值目标值
|
||||||
|
duration?: number // 数值动画持续时间,单位 ms
|
||||||
|
autoplay?: boolean // 是否自动开始动画
|
||||||
|
precision?: number // 精度,保留小数点后几位
|
||||||
|
prefix?: string // 前缀
|
||||||
|
suffix?: string // 后缀
|
||||||
|
separator?: string // 千分位分隔符
|
||||||
|
decimal?: string // 小数点字符
|
||||||
|
valueStyle?: CSSProperties // 数值文本样式
|
||||||
|
transition?: TransitionFunc // 动画过渡效果
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
from: 0,
|
||||||
|
to: 1000,
|
||||||
|
duration: 3000,
|
||||||
|
autoplay: true,
|
||||||
|
precision: 0,
|
||||||
|
prefix: undefined,
|
||||||
|
suffix: undefined,
|
||||||
|
separator: ',',
|
||||||
|
decimal: '.',
|
||||||
|
valueStyle: () => ({}),
|
||||||
|
transition: TransitionFunc['easeInOutCubic']
|
||||||
|
});
|
||||||
|
const source = ref(props.from);
|
||||||
|
const emits = defineEmits(['started', 'finished']);
|
||||||
|
watchEffect(() => {
|
||||||
|
source.value = props.from;
|
||||||
|
});
|
||||||
|
watch([() => props.from, () => props.to], () => {
|
||||||
|
if (props.autoplay) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.autoplay) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const outputValue = useTransition(source, {
|
||||||
|
duration: props.duration,
|
||||||
|
transition: TransitionPresets[props.transition],
|
||||||
|
onFinished: () => emits('finished'),
|
||||||
|
onStarted: () => emits('started')
|
||||||
|
});
|
||||||
|
|
||||||
|
function play() {
|
||||||
|
source.value = props.to;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showValue = computed(() => {
|
||||||
|
const {precision, separator, decimal, prefix, suffix} = props;
|
||||||
|
return formatNumber(outputValue.value, precision, separator, decimal, prefix, suffix);
|
||||||
|
});
|
||||||
|
defineExpose({
|
||||||
|
play
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<span :style="valueStyle">
|
||||||
|
{{ showValue }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
571
src/components/MyUI/Pagination/Pagination.vue
Normal file
571
src/components/MyUI/Pagination/Pagination.vue
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, nextTick, ref, watch} from 'vue';
|
||||||
|
import Input from '../Input/Input.vue';
|
||||||
|
import Select from '../Select/Select.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page?: number // (v-model) 当前页数
|
||||||
|
pageSize?: number // (v-model) 每页条数
|
||||||
|
total?: number // 数据总数
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
pageAmount?: number // 显示的页码数
|
||||||
|
hideOnSinglePage?: boolean // 只有一页时是否隐藏分页
|
||||||
|
showQuickJumper?: boolean // 是否可以快速跳转至某页
|
||||||
|
showSizeChanger?: boolean // 是否展示 pageSize 切换器,当 total 大于 50 时默认为 true
|
||||||
|
pageSizeOptions?: string[] | number[] // 设置每页可以显示多少条
|
||||||
|
showTotal?: boolean | ((total: number, range: number[]) => string) // 用于显示数据总量和当前数据顺序
|
||||||
|
placement?: 'left' | 'center' | 'right' // 分页展示位置,靠左 left,居中 center,靠右 right
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
disabled: false,
|
||||||
|
pageAmount: 5,
|
||||||
|
hideOnSinglePage: false,
|
||||||
|
showQuickJumper: false,
|
||||||
|
showSizeChanger: undefined,
|
||||||
|
pageSizeOptions: () => [10, 20, 50, 100],
|
||||||
|
showTotal: false,
|
||||||
|
placement: 'center'
|
||||||
|
});
|
||||||
|
const currentPage = ref(props.page); // 当前 page
|
||||||
|
const currentPageSize = ref(props.pageSize); // 当前 pageSize
|
||||||
|
const jumpNumber = ref(); // 跳转的页码
|
||||||
|
const forwardMore = ref(false); // 左省略号展示
|
||||||
|
const backwardMore = ref(false); // 右省略号展示
|
||||||
|
const emits = defineEmits(['update:page', 'update:pageSize', 'change', 'pageSizeChange']);
|
||||||
|
const totalPage = computed(() => {
|
||||||
|
// 总页数
|
||||||
|
return Math.ceil(props.total / currentPageSize.value); // 向上取整
|
||||||
|
});
|
||||||
|
const totalText = computed(() => {
|
||||||
|
if (typeof props.showTotal === 'boolean') {
|
||||||
|
if (props.showTotal) {
|
||||||
|
return `共 ${props.total} 条`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const first = (currentPage.value - 1) * currentPageSize.value + 1;
|
||||||
|
const last =
|
||||||
|
currentPage.value * currentPageSize.value > props.total ? props.total : currentPage.value * currentPageSize.value;
|
||||||
|
return props.showTotal(props.total, [first, last]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const pageList = computed(() => {
|
||||||
|
// 获取显示的页码数组
|
||||||
|
return dealPageList(currentPage.value).filter((n) => n !== 1 && n !== totalPage.value);
|
||||||
|
});
|
||||||
|
const showPageSizeChanger = computed(() => {
|
||||||
|
if (typeof props.showSizeChanger === 'boolean') {
|
||||||
|
return props.showSizeChanger;
|
||||||
|
} else {
|
||||||
|
// undefined
|
||||||
|
return props.total > 50;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selectOptions = computed(() => {
|
||||||
|
const pageSizeOptipns = [currentPageSize.value, ...props.pageSizeOptions].map((pageSize: number | string) =>
|
||||||
|
Number(pageSize)
|
||||||
|
);
|
||||||
|
return [...new Set(pageSizeOptipns)]
|
||||||
|
.sort((a: number, b: number) => a - b)
|
||||||
|
.map((pageSize: number) => {
|
||||||
|
return {
|
||||||
|
label: `${pageSize} 条/页`,
|
||||||
|
value: pageSize
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.page,
|
||||||
|
(to: number) => {
|
||||||
|
currentPage.value = to;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.pageSize,
|
||||||
|
(to: number) => {
|
||||||
|
currentPageSize.value = to;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function dealPageList(curPage: number): number[] {
|
||||||
|
var resList: number[] = [];
|
||||||
|
var offset = Math.floor(props.pageAmount / 2); // 向下取整
|
||||||
|
var pager = {
|
||||||
|
start: curPage - offset,
|
||||||
|
end: curPage + offset
|
||||||
|
};
|
||||||
|
if (pager.start < 1) {
|
||||||
|
pager.end = pager.end + (1 - pager.start);
|
||||||
|
pager.start = 1;
|
||||||
|
}
|
||||||
|
if (pager.end > totalPage.value) {
|
||||||
|
pager.start = pager.start - (pager.end - totalPage.value);
|
||||||
|
pager.end = totalPage.value;
|
||||||
|
}
|
||||||
|
if (pager.start < 1) {
|
||||||
|
pager.start = 1;
|
||||||
|
}
|
||||||
|
if (pager.start > 1) {
|
||||||
|
forwardMore.value = true;
|
||||||
|
} else {
|
||||||
|
forwardMore.value = false;
|
||||||
|
}
|
||||||
|
if (pager.end < totalPage.value) {
|
||||||
|
backwardMore.value = true;
|
||||||
|
} else {
|
||||||
|
backwardMore.value = false;
|
||||||
|
}
|
||||||
|
// 生成要显示的页码数组
|
||||||
|
for (let i: number = pager.start; i <= pager.end; i++) {
|
||||||
|
resList.push(i);
|
||||||
|
}
|
||||||
|
return resList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageForward(): void {
|
||||||
|
currentPage.value = currentPage.value - props.pageAmount > 0 ? currentPage.value - props.pageAmount : 1;
|
||||||
|
emits('update:page', currentPage.value);
|
||||||
|
emits('change', currentPage.value, currentPageSize.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageBackward(): void {
|
||||||
|
currentPage.value =
|
||||||
|
currentPage.value + props.pageAmount < totalPage.value ? currentPage.value + props.pageAmount : totalPage.value;
|
||||||
|
emits('update:page', currentPage.value);
|
||||||
|
emits('change', currentPage.value, currentPageSize.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onPageJump() {
|
||||||
|
let num = Number(jumpNumber.value); // 转换为数字
|
||||||
|
if (jumpNumber.value && Number.isInteger(num)) {
|
||||||
|
// 是否为整数
|
||||||
|
if (num < 1) {
|
||||||
|
num = 1;
|
||||||
|
}
|
||||||
|
if (num > totalPage.value) {
|
||||||
|
num = totalPage.value;
|
||||||
|
}
|
||||||
|
onPageChange(num);
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
jumpNumber.value = undefined; // 清空跳转输入框
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageChange(page: number): boolean | void {
|
||||||
|
if (page === 0 || page === totalPage.value + 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentPage.value !== page) {
|
||||||
|
// 点击的页码不是当前页码
|
||||||
|
currentPage.value = page;
|
||||||
|
emits('update:page', currentPage.value);
|
||||||
|
emits('change', currentPage.value, currentPageSize.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageSizeChange(pageSize: number) {
|
||||||
|
currentPageSize.value = pageSize;
|
||||||
|
const maxPage = Math.ceil(props.total / pageSize);
|
||||||
|
if (currentPage.value > maxPage) {
|
||||||
|
currentPage.value = maxPage;
|
||||||
|
}
|
||||||
|
emits('update:page', currentPage.value);
|
||||||
|
emits('update:pageSize', currentPageSize.value);
|
||||||
|
emits('pageSizeChange', currentPage.value, currentPageSize.value);
|
||||||
|
emits('change', currentPage.value, currentPageSize.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-pagination"
|
||||||
|
:class="[
|
||||||
|
`pagination-${placement}`,
|
||||||
|
{
|
||||||
|
'pagination-disabled': disabled,
|
||||||
|
'pagination-hidden': !total || (hideOnSinglePage && total <= currentPageSize)
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="pagination-total-text" v-if="totalText">{{ totalText }}</span>
|
||||||
|
<span
|
||||||
|
tabindex="0"
|
||||||
|
class="pagination-prev"
|
||||||
|
:class="{ 'item-disabled': currentPage === 1 }"
|
||||||
|
@keydown.enter.prevent="disabled ? () => false : onPageChange(currentPage - 1)"
|
||||||
|
@click="disabled || currentPage === 1 ? () => false : onPageChange(currentPage - 1)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="arrow-svg"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="left"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 0 0 0 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
tabindex="0"
|
||||||
|
:class="['pagination-item', { 'item-active': currentPage === 1 }]"
|
||||||
|
@click="disabled ? () => false : onPageChange(1)"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-show="forwardMore && pageList[0] - 1 > 1"
|
||||||
|
tabindex="0"
|
||||||
|
ref="forward"
|
||||||
|
class="pagintion-item-link"
|
||||||
|
@click="disabled ? () => false : onPageForward()"
|
||||||
|
>
|
||||||
|
<span class="u-ellipsis">•••</span>
|
||||||
|
<svg class="u-icon" viewBox="64 64 896 896" data-icon="double-left" aria-hidden="true" focusable="false">
|
||||||
|
<path
|
||||||
|
d="M272.9 512l265.4-339.1c4.1-5.2.4-12.9-6.3-12.9h-77.3c-4.9 0-9.6 2.3-12.6 6.1L186.8 492.3a31.99 31.99 0 0 0 0 39.5l255.3 326.1c3 3.9 7.7 6.1 12.6 6.1H532c6.7 0 10.4-7.7 6.3-12.9L272.9 512zm304 0l265.4-339.1c4.1-5.2.4-12.9-6.3-12.9h-77.3c-4.9 0-9.6 2.3-12.6 6.1L490.8 492.3a31.99 31.99 0 0 0 0 39.5l255.3 326.1c3 3.9 7.7 6.1 12.6 6.1H836c6.7 0 10.4-7.7 6.3-12.9L576.9 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
tabindex="0"
|
||||||
|
:class="['pagination-item', { 'item-active': currentPage === page }]"
|
||||||
|
v-for="(page, index) in pageList"
|
||||||
|
:key="index"
|
||||||
|
@click="disabled ? () => false : onPageChange(page)"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-show="backwardMore && pageList[pageList.length - 1] + 1 < totalPage"
|
||||||
|
tabindex="0"
|
||||||
|
ref="backward"
|
||||||
|
class="pagintion-item-link"
|
||||||
|
@click="disabled ? () => false : onPageBackward()"
|
||||||
|
>
|
||||||
|
<span class="u-ellipsis">•••</span>
|
||||||
|
<svg class="u-icon" viewBox="64 64 896 896" data-icon="double-right" aria-hidden="true" focusable="false">
|
||||||
|
<path
|
||||||
|
d="M533.2 492.3L277.9 166.1c-3-3.9-7.7-6.1-12.6-6.1H188c-6.7 0-10.4 7.7-6.3 12.9L447.1 512 181.7 851.1A7.98 7.98 0 0 0 188 864h77.3c4.9 0 9.6-2.3 12.6-6.1l255.3-326.1c9.1-11.7 9.1-27.9 0-39.5zm304 0L581.9 166.1c-3-3.9-7.7-6.1-12.6-6.1H492c-6.7 0-10.4 7.7-6.3 12.9L751.1 512 485.7 851.1A7.98 7.98 0 0 0 492 864h77.3c4.9 0 9.6-2.3 12.6-6.1l255.3-326.1c9.1-11.7 9.1-27.9 0-39.5z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-show="totalPage !== 1"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['pagination-item', { 'item-active': currentPage === totalPage }]"
|
||||||
|
@click="disabled ? () => false : onPageChange(totalPage)"
|
||||||
|
>
|
||||||
|
{{ totalPage }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
tabindex="0"
|
||||||
|
class="pagination-next"
|
||||||
|
:class="{ 'item-disabled': currentPage === totalPage }"
|
||||||
|
@keydown.enter.prevent="disabled ? () => false : onPageChange(currentPage + 1)"
|
||||||
|
@click="disabled || currentPage === totalPage ? () => false : onPageChange(currentPage + 1)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="arrow-svg"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="right"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 0 0 302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 0 0 0-50.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="m-pagination-options" v-if="showPageSizeChanger || showQuickJumper">
|
||||||
|
<Select
|
||||||
|
v-if="showPageSizeChanger"
|
||||||
|
:class="{ mr8: showQuickJumper }"
|
||||||
|
:disabled="disabled"
|
||||||
|
:options="selectOptions"
|
||||||
|
@change="onPageSizeChange"
|
||||||
|
v-model="currentPageSize"
|
||||||
|
/>
|
||||||
|
<span class="pagination-jump-page" v-if="showQuickJumper">
|
||||||
|
跳至<Input
|
||||||
|
:width="50"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-model:value.lazy="jumpNumber"
|
||||||
|
@change="onPageJump"
|
||||||
|
@enter="onPageJump"
|
||||||
|
/>页
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
|
||||||
|
.pagination-total-text {
|
||||||
|
display: inline-block;
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-item {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 30px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
margin-right: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
user-select: none; // 禁止选取文本
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.item-active();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-prev,
|
||||||
|
.pagination-next {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 30px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
user-select: none; // 禁止选取文本
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
.arrow-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
fill: currentColor;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: @themeColor;
|
||||||
|
|
||||||
|
.arrow-svg {
|
||||||
|
color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-prev {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-active {
|
||||||
|
// 悬浮/选中样式
|
||||||
|
font-weight: 600;
|
||||||
|
color: @themeColor;
|
||||||
|
border-color: @themeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
background: #fff;
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
|
||||||
|
.arrow-svg {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-svg {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagintion-item-link {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.u-ellipsis {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
line-height: 32px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-align: center;
|
||||||
|
text-indent: 0.13em;
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
display: inline-block;
|
||||||
|
fill: @themeColor;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.u-ellipsis {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-icon {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-pagination-options {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 16px;
|
||||||
|
|
||||||
|
.mr8 {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-jump-page {
|
||||||
|
display: inline-block;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
|
||||||
|
.m-input-wrap {
|
||||||
|
margin: 0 8px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-disabled {
|
||||||
|
.pagination-prev,
|
||||||
|
.pagination-next {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
border-color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.arrow-svg {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
|
.arrow-svg {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-item {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
border-color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
font-weight: normal;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
border-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-active {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagintion-item-link {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.u-ellipsis {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-icon {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-pagination-options {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
226
src/components/MyUI/Popconfirm/Popconfirm.vue
Normal file
226
src/components/MyUI/Popconfirm/Popconfirm.vue
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties, Slot, VNode} from 'vue';
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string // 弹出确认框的标题 string | slot
|
||||||
|
titleStyle?: CSSProperties // 设置标题的样式
|
||||||
|
description?: string // 弹出确认框的内容描述 string | slot
|
||||||
|
descriptionStyle?: CSSProperties // 设置内容描述的样式
|
||||||
|
tooltipStyle?: CSSProperties // 设置弹出提示的样式
|
||||||
|
icon?: 'success' | 'info' | 'warning' | 'danger' | VNode | Slot // 自定义 Icon 图标,预置四种类型图标 string | VNode | slot
|
||||||
|
iconStyle?: CSSProperties // 设置 Icon 图标的样式,一般不需要设置,主要用于自定义 Icon 图标时
|
||||||
|
cancelText?: string // 取消按钮文字 string | slot
|
||||||
|
cancelType?: 'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link' // 取消按钮类型
|
||||||
|
cancelProps?: object // 取消按钮 props,优先级高于 cancelType,参考 Button 组件 props
|
||||||
|
okText?: string // 确认按钮文字 string | slot
|
||||||
|
okType?: 'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link' // 确认按钮类型
|
||||||
|
okProps?: object // 确认按钮 props,优先级高于 okType,参考 Button 组件 props
|
||||||
|
showCancel?: boolean // 是否显示取消按钮
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: undefined,
|
||||||
|
titleStyle: () => ({}),
|
||||||
|
description: undefined,
|
||||||
|
descriptionStyle: () => ({}),
|
||||||
|
tooltipStyle: () => ({}),
|
||||||
|
icon: 'warning',
|
||||||
|
iconStyle: () => ({}),
|
||||||
|
cancelText: '取消',
|
||||||
|
cancelType: 'default',
|
||||||
|
cancelProps: () => ({}),
|
||||||
|
okText: '确定',
|
||||||
|
okType: 'primary',
|
||||||
|
okProps: () => ({}),
|
||||||
|
showCancel: true
|
||||||
|
});
|
||||||
|
const tooltipRef = ref();
|
||||||
|
const emits = defineEmits(['cancel', 'ok']);
|
||||||
|
const slotsExist = useSlotsExist(['description']);
|
||||||
|
const showDesc = computed(() => {
|
||||||
|
return slotsExist.description || props.description;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onCancel(e: Event) {
|
||||||
|
emits('cancel', e);
|
||||||
|
tooltipRef.value.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOk(e: Event) {
|
||||||
|
emits('ok', e);
|
||||||
|
tooltipRef.value.hide();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Tooltip
|
||||||
|
ref="tooltipRef"
|
||||||
|
max-width="auto"
|
||||||
|
bg-color="#fff"
|
||||||
|
:tooltip-style="{
|
||||||
|
padding: '12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
textAlign: 'start',
|
||||||
|
...tooltipStyle
|
||||||
|
}"
|
||||||
|
trigger="click"
|
||||||
|
:transition-duration="200"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<template #tooltip>
|
||||||
|
<div class="m-popconfirm-message">
|
||||||
|
<span class="m-popconfirm-icon" :style="iconStyle">
|
||||||
|
<slot name="icon">
|
||||||
|
<svg
|
||||||
|
v-if="icon === 'info'"
|
||||||
|
class="icon-info"
|
||||||
|
focusable="false"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="info-circle"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 0 1 0-96 48.01 48.01 0 0 1 0 96z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="icon === 'success'"
|
||||||
|
class="icon-success"
|
||||||
|
focusable="false"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="check-circle"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 0 1-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="icon === 'danger'"
|
||||||
|
class="icon-danger"
|
||||||
|
focusable="false"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="close-circle"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 0 1-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="icon === 'warning'"
|
||||||
|
class="icon-warning"
|
||||||
|
focusable="false"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
data-icon="exclamation-circle"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 0 1 0-96 48.01 48.01 0 0 1 0 96z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<component v-else-if="icon" :is="icon"/>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
<div class="popconfirm-title" :class="{ 'title-font-weight': showDesc }" :style="titleStyle">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showDesc" class="popconfirm-description" :style="descriptionStyle">
|
||||||
|
<slot name="description">{{ description }}</slot>
|
||||||
|
</div>
|
||||||
|
<div class="popconfirm-buttons">
|
||||||
|
<Button v-if="showCancel" size="small" :type="cancelType" @click="onCancel" v-bind="cancelProps">
|
||||||
|
<slot name="cancelText">{{ cancelText }}</slot>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" :type="okType" @click="onOk" v-bind="okProps">
|
||||||
|
<slot name="okText">{{ okText }}</slot>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<slot></slot>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-popconfirm-message {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
.m-popconfirm-icon {
|
||||||
|
flex: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
padding-top: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.icon-svg {
|
||||||
|
display: inline-block;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-info {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-success {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-danger {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-warning {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popconfirm-title {
|
||||||
|
flex: auto;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-font-weight {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popconfirm-description {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 22px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popconfirm-buttons {
|
||||||
|
text-align: end;
|
||||||
|
|
||||||
|
.m-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {CSSProperties} from 'vue';
|
import type {CSSProperties} from 'vue';
|
||||||
import {computed} from 'vue';
|
import {computed} from 'vue';
|
||||||
import {useSlotsExist} from '../utils';
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string // 卡片标题 string | slot
|
title?: string // 卡片标题 string | slot
|
||||||
@@ -42,7 +42,7 @@ const showContent = computed(() => {
|
|||||||
transform: `translate(${props.offsetX}px, 0)`,
|
transform: `translate(${props.offsetX}px, 0)`,
|
||||||
backgroundColor: 'var(--background-color)',
|
backgroundColor: 'var(--background-color)',
|
||||||
color: 'var(--text-color)',
|
color: 'var(--text-color)',
|
||||||
border: '1px solid var(--text-color)',
|
border: '1px solid var(--white-color)',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||||
...tooltipStyle
|
...tooltipStyle
|
||||||
}"
|
}"
|
||||||
|
|||||||
351
src/components/MyUI/Progress/Progress.vue
Normal file
351
src/components/MyUI/Progress/Progress.vue
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Gradient {
|
||||||
|
'0%'?: string
|
||||||
|
'100%'?: string
|
||||||
|
from?: string
|
||||||
|
to?: string
|
||||||
|
direction?: 'left' | 'right' // 默认 'right'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width?: number | string // 进度条总宽度,单位 px
|
||||||
|
percent?: number // 当前进度百分比
|
||||||
|
strokeWidth?: number // 进度条线的宽度,单位 px,当 type: 'circle' 时,单位是进度圈画布宽度的百分比
|
||||||
|
strokeColor?: string | Gradient // 进度条的色彩,传入 string 时为纯色,传入 Gradient 时为渐变,进度圈时 direction: 'left' 为逆时针,direction: 'right' 为顺时针
|
||||||
|
strokeLinecap?: 'round' | 'butt' | 'square' // 进度条的样式
|
||||||
|
showInfo?: boolean // 是否显示进度数值或状态图标
|
||||||
|
success?: string // 进度完成时的信息 string | slot
|
||||||
|
format?: (percent: number) => string | number // 内容的模板函数 function | slot
|
||||||
|
type?: 'line' | 'circle' // 进度条类型
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: '100%',
|
||||||
|
percent: 0,
|
||||||
|
strokeWidth: 8,
|
||||||
|
strokeColor: '#1677FF',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
showInfo: true,
|
||||||
|
success: undefined,
|
||||||
|
format: (percent: number) => percent + '%',
|
||||||
|
type: 'line'
|
||||||
|
});
|
||||||
|
const slotsExist = useSlotsExist(['success']);
|
||||||
|
const totalWidth = computed(() => {
|
||||||
|
// 进度条总宽度
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const perimeter = computed(() => {
|
||||||
|
// 圆条周长
|
||||||
|
return (100 - props.strokeWidth) * Math.PI;
|
||||||
|
});
|
||||||
|
const path = computed(() => {
|
||||||
|
// 圆条轨道路径指令
|
||||||
|
const long = 100 - props.strokeWidth;
|
||||||
|
return `M 50,50 m 0,-${long / 2}
|
||||||
|
a ${long / 2},${long / 2} 0 1 1 0,${long}
|
||||||
|
a ${long / 2},${long / 2} 0 1 1 0,-${long}`;
|
||||||
|
});
|
||||||
|
const gradientColor = computed(() => {
|
||||||
|
// 是否为渐变色
|
||||||
|
return typeof props.strokeColor !== 'string';
|
||||||
|
});
|
||||||
|
const lineColor = computed(() => {
|
||||||
|
if (typeof props.strokeColor === 'string') {
|
||||||
|
return props.strokeColor;
|
||||||
|
} else {
|
||||||
|
return `linear-gradient(to ${props.strokeColor.direction || 'right'}, ${props.strokeColor['0%'] || props.strokeColor.from}, ${props.strokeColor['100%'] || props.strokeColor.to})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const circleColorFrom = computed(() => {
|
||||||
|
if (gradientColor.value) {
|
||||||
|
const gradientColor = props.strokeColor as Gradient;
|
||||||
|
if (!gradientColor.direction || gradientColor.direction === 'right') {
|
||||||
|
return gradientColor['0%'] || gradientColor.from;
|
||||||
|
} else {
|
||||||
|
return gradientColor['100%'] || gradientColor.to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props.strokeColor;
|
||||||
|
});
|
||||||
|
const circleColorTo = computed(() => {
|
||||||
|
if (gradientColor.value) {
|
||||||
|
const gradientColor = props.strokeColor as Gradient;
|
||||||
|
if (!gradientColor.direction || gradientColor.direction === 'right') {
|
||||||
|
return gradientColor['100%'] || gradientColor.to;
|
||||||
|
} else {
|
||||||
|
return gradientColor['0%'] || gradientColor.from;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props.strokeColor;
|
||||||
|
});
|
||||||
|
const showPercent = computed(() => {
|
||||||
|
return props.format(props.percent > 100 ? 100 : props.percent);
|
||||||
|
});
|
||||||
|
const showSuccess = computed(() => {
|
||||||
|
return slotsExist.success || props.success;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="type === 'line'"
|
||||||
|
class="m-progress-line"
|
||||||
|
:style="`width: ${totalWidth}; height: ${strokeWidth < 24 ? 24 : strokeWidth}px;`"
|
||||||
|
>
|
||||||
|
<div class="m-progress-inner">
|
||||||
|
<div
|
||||||
|
:class="['progress-bg', { 'line-success': percent >= 100 && !gradientColor }]"
|
||||||
|
:style="`background: ${lineColor}; width: ${percent >= 100 ? 100 : percent}%; height: ${strokeWidth}px; --border-radius: ${strokeLinecap === 'round' ? '100px' : 0};`"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<template v-if="showInfo">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<span v-if="percent >= 100" class="progress-success">
|
||||||
|
<svg
|
||||||
|
v-if="showSuccess === undefined"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="check-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<p v-else class="progress-success-info">
|
||||||
|
<slot name="success">{{ success }}</slot>
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
<p v-else class="progress-text">
|
||||||
|
<slot name="format" :percent="percent">{{ showPercent }}</slot>
|
||||||
|
</p>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-else class="m-progress-circle" :style="`width: ${totalWidth}; height: ${totalWidth};`">
|
||||||
|
<svg class="progress-circle" viewBox="0 0 100 100">
|
||||||
|
<defs v-if="gradientColor">
|
||||||
|
<linearGradient id="circleGradient" x1="100%" y1="0%" x2="0%" y2="0%">
|
||||||
|
<stop offset="0%" :stop-color="circleColorFrom as string"></stop>
|
||||||
|
<stop offset="100%" :stop-color="circleColorTo as string"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
:d="path"
|
||||||
|
:stroke-linecap="strokeLinecap"
|
||||||
|
class="circle-trail"
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
:style="`stroke-dasharray: ${perimeter}px, ${perimeter}px;`"
|
||||||
|
fill-opacity="0"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
:d="path"
|
||||||
|
:stroke-linecap="strokeLinecap"
|
||||||
|
class="circle-path"
|
||||||
|
:class="{ 'circle-path-success': percent >= 100 && !gradientColor }"
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
:stroke="gradientColor ? 'url(#circleGradient)' : lineColor"
|
||||||
|
:style="`stroke-dasharray: ${(percent / 100) * perimeter}px, ${perimeter}px;`"
|
||||||
|
:opacity="percent === 0 ? 0 : 1"
|
||||||
|
fill-opacity="0"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<template v-if="showInfo">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<svg
|
||||||
|
v-if="showSuccess === undefined && percent >= 100"
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="check"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<p v-else-if="percent >= 100" class="progress-success-info">
|
||||||
|
<slot name="success">{{ success }}</slot>
|
||||||
|
</p>
|
||||||
|
<p v-else class="progress-text">
|
||||||
|
<slot name="format" :percent="percent">{{ showPercent }}</slot>
|
||||||
|
</p>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@success: #52c41a;
|
||||||
|
.m-progress-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.m-progress-inner {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.progress-bg {
|
||||||
|
position: relative;
|
||||||
|
background-color: @themeColor;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
background-image: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.5) 100%);
|
||||||
|
animation: progressRipple 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progressRipple {
|
||||||
|
0% {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
right: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-success {
|
||||||
|
background: @success !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-success {
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 8px;
|
||||||
|
flex-shrink: 0; // 默认 1.即空间不足时,项目将缩小
|
||||||
|
.icon-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
color: @success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-success-info {
|
||||||
|
flex-shrink: 0; // 默认 1.即空间不足时,项目将缩小
|
||||||
|
width: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 8px;
|
||||||
|
color: @success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
/*
|
||||||
|
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小
|
||||||
|
如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
|
||||||
|
*/
|
||||||
|
flex-shrink: 0; // 默认 1.即空间不足时,项目将缩小
|
||||||
|
width: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 8px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-progress-circle {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.progress-circle {
|
||||||
|
.circle-trail {
|
||||||
|
stroke: rgba(0, 0, 0, 0.06);
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
transition: stroke-dashoffset 0.3s ease 0s,
|
||||||
|
stroke-dasharray 0.3s ease 0s,
|
||||||
|
stroke 0.3s ease 0s,
|
||||||
|
stroke-width 0.06s ease 0.3s,
|
||||||
|
opacity 0.3s ease 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-path {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
transition: stroke-dashoffset 0.3s ease 0s,
|
||||||
|
stroke-dasharray 0.3s ease 0s,
|
||||||
|
stroke 0.3s ease 0s,
|
||||||
|
stroke-width 0.06s ease 0.3s,
|
||||||
|
opacity 0.3s ease 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-path-success {
|
||||||
|
stroke: @success !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: inline-block;
|
||||||
|
width: 30%;
|
||||||
|
height: 30%;
|
||||||
|
fill: currentColor;
|
||||||
|
color: @success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-success-info {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 100%;
|
||||||
|
font-size: 27px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
color: @success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 100%;
|
||||||
|
font-size: 27px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
76
src/components/MyUI/QRCode/QRCode.vue
Normal file
76
src/components/MyUI/QRCode/QRCode.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {useQRCode} from '@vueuse/integrations/useQRCode';
|
||||||
|
|
||||||
|
/*
|
||||||
|
参考文档:https://vueuse.org/integrations/useQRCode/
|
||||||
|
https://www.npmjs.com/package/qrcode#qr-code-options
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
value?: string // 扫描后的文本或地址
|
||||||
|
size?: number // 二维码大小,单位 px
|
||||||
|
color?: string // 二维码颜色,Value must be in hex format (十六进制颜色值)
|
||||||
|
bgColor?: string // 二维码背景色,Value must be in hex format (十六进制颜色值)
|
||||||
|
bordered?: boolean // 是否有边框
|
||||||
|
borderColor?: string // 边框颜色
|
||||||
|
scale?: number // 每个 black dots 多少像素
|
||||||
|
/*
|
||||||
|
纠错等级也叫纠错率,就是指二维码可以被遮挡后还能正常扫描,而这个能被遮挡的最大面积就是纠错率。
|
||||||
|
通常情况下二维码分为 4 个纠错级别:L级 可纠正约 7% 错误、M级 可纠正约 15% 错误、Q级 可纠正约 25% 错误、H级 可纠正约30% 错误。
|
||||||
|
并不是所有位置都可以缺损,像最明显的三个角上的方框,直接影响初始定位。中间零散的部分是内容编码,可以容忍缺损。
|
||||||
|
当二维码的内容编码携带信息比较少的时候,也就是链接比较短的时候,设置不同的纠错等级,生成的图片不会发生变化。
|
||||||
|
*/
|
||||||
|
errorLevel?: 'L' | 'M' | 'Q' | 'H' // 二维码纠错等级
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
value: undefined,
|
||||||
|
size: 160,
|
||||||
|
color: '#000',
|
||||||
|
bgColor: '#FFF',
|
||||||
|
bordered: true,
|
||||||
|
borderColor: '#0505050f',
|
||||||
|
scale: 8,
|
||||||
|
errorLevel: 'H' // 可选 L M Q H
|
||||||
|
});
|
||||||
|
const qrcode = computed(() => {
|
||||||
|
// `qrcode` will be a ref of data URL
|
||||||
|
return useQRCode(props.value || '', {
|
||||||
|
errorCorrectionLevel: props.errorLevel,
|
||||||
|
type: 'image/png',
|
||||||
|
quality: 1,
|
||||||
|
margin: 3,
|
||||||
|
scale: props.scale, // 8px per modules(black dots)
|
||||||
|
color: {
|
||||||
|
dark: props.color, // 像素点颜色
|
||||||
|
light: props.bgColor // 背景色
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-qrcode"
|
||||||
|
:class="{ 'qrcode-bordered': bordered }"
|
||||||
|
:style="`width: ${size}px; height: ${size}px; border-color: ${borderColor};`"
|
||||||
|
>
|
||||||
|
<img :src="qrcode.value" class="qrcode-image" alt="QRCode"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-qrcode {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.qrcode-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode-bordered {
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
396
src/components/MyUI/Radio/Radio.vue
Normal file
396
src/components/MyUI/Radio/Radio.vue
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, watchEffect} from 'vue';
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string // 选项名
|
||||||
|
value: string | number | boolean // 选项值
|
||||||
|
disabled?: boolean // 是否禁用选项
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options?: Option[] // 单选框选项数据
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
vertical?: boolean // 是否垂直排列,仅当 button: false 时生效
|
||||||
|
checked?: boolean // (v-model) 当前是否选中
|
||||||
|
gap?: number | number[] // 多个单选框之间的间距;垂直排列时为垂直间距,单位 px;数组间距用于水平排列折行时:[水平间距, 垂直间距];仅当 button: false 时生效
|
||||||
|
width?: string | number // 单选区域最大宽度,超出后折行,单位 px;仅当 button: false 时生效
|
||||||
|
height?: string | number // 单选区域最大高度,超出后滚动,单位 px;仅当 button: false 时生效
|
||||||
|
button?: boolean // 是否启用按钮样式
|
||||||
|
buttonStyle?: 'outline' | 'solid' // 按钮样式风格
|
||||||
|
buttonSize?: 'small' | 'middle' | 'large' // 按钮大小;仅当 button: true 时生效
|
||||||
|
value?: string | number | boolean // (v-model) 当前选中的值
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
options: () => [],
|
||||||
|
disabled: false,
|
||||||
|
vertical: false,
|
||||||
|
checked: false,
|
||||||
|
gap: 8,
|
||||||
|
width: 'auto',
|
||||||
|
height: 'auto',
|
||||||
|
button: false,
|
||||||
|
buttonStyle: 'outline',
|
||||||
|
buttonSize: 'middle',
|
||||||
|
value: undefined
|
||||||
|
});
|
||||||
|
const radioChecked = ref<boolean>();
|
||||||
|
const optionsCheckedValue = ref<string | number | boolean>();
|
||||||
|
const emits = defineEmits(['update:checked', 'update:value', 'change']);
|
||||||
|
const optionsAmount = computed(() => {
|
||||||
|
// 选项总数
|
||||||
|
return props.options.length;
|
||||||
|
});
|
||||||
|
const maxWidth = computed(() => {
|
||||||
|
// 单选区域最大宽度
|
||||||
|
if (!props.button) {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'auto';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const maxHeight = computed(() => {
|
||||||
|
// 单选区域最大高度
|
||||||
|
if (!props.button) {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
} else {
|
||||||
|
return props.height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'auto';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const gapValue = computed(() => {
|
||||||
|
if (!props.button) {
|
||||||
|
if (!props.vertical && Array.isArray(props.gap)) {
|
||||||
|
return `${props.gap[1]}px ${props.gap[0]}px`;
|
||||||
|
}
|
||||||
|
return `${props.gap}px`;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
radioChecked.value = props.checked;
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
optionsCheckedValue.value = props.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkDisabled(disabled: boolean | undefined) {
|
||||||
|
if (disabled === undefined) {
|
||||||
|
return props.disabled;
|
||||||
|
} else {
|
||||||
|
return disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(value: string | number | boolean) {
|
||||||
|
if (value !== optionsCheckedValue.value) {
|
||||||
|
optionsCheckedValue.value = value;
|
||||||
|
emits('update:value', value);
|
||||||
|
emits('change', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChecked() {
|
||||||
|
if (!radioChecked.value) {
|
||||||
|
radioChecked.value = true;
|
||||||
|
emits('update:checked', true);
|
||||||
|
emits('change', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-radio"
|
||||||
|
:class="{
|
||||||
|
'radio-vertical': !button && vertical,
|
||||||
|
'radio-button-solid': buttonStyle === 'solid',
|
||||||
|
'radio-button-small': button && buttonSize === 'small',
|
||||||
|
'radio-button-large': button && buttonSize === 'large'
|
||||||
|
}"
|
||||||
|
:style="`--radio-gap: ${gapValue}; --radio-max-width: ${maxWidth}; --radio-max-height: ${maxHeight};`"
|
||||||
|
>
|
||||||
|
<template v-if="optionsAmount">
|
||||||
|
<template v-if="!button">
|
||||||
|
<div
|
||||||
|
class="m-radio-wrap"
|
||||||
|
:class="{ 'radio-disabled': checkDisabled(option.disabled) }"
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="index"
|
||||||
|
@click="checkDisabled(option.disabled) ? () => false : onClick(option.value)"
|
||||||
|
>
|
||||||
|
<span class="radio-handle" :class="{ 'radio-checked': optionsCheckedValue === option.value }"></span>
|
||||||
|
<span class="radio-label">
|
||||||
|
<slot :label="option.label">{{ option.label }}</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="m-radio-button-wrap"
|
||||||
|
:class="{
|
||||||
|
'radio-button-checked': optionsCheckedValue === option.value,
|
||||||
|
'radio-button-disabled': checkDisabled(option.disabled)
|
||||||
|
}"
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="index"
|
||||||
|
@click="checkDisabled(option.disabled) ? () => false : onClick(option.value)"
|
||||||
|
>
|
||||||
|
<span class="radio-label">
|
||||||
|
<slot :label="option.label">{{ option.label }}</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<template v-if="!button">
|
||||||
|
<div class="m-radio-wrap" :class="{ 'radio-disabled': disabled }" @click="disabled ? () => false : onChecked()">
|
||||||
|
<span class="radio-handle" :class="{ 'radio-checked': radioChecked }"></span>
|
||||||
|
<span class="radio-label">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="m-radio-button-wrap"
|
||||||
|
:class="{
|
||||||
|
'radio-button-checked': radioChecked,
|
||||||
|
'radio-button-disabled': disabled
|
||||||
|
}"
|
||||||
|
@click="disabled ? () => false : onChecked()"
|
||||||
|
>
|
||||||
|
<span class="radio-label">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-radio {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--radio-gap);
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
max-width: var(--radio-max-width);
|
||||||
|
max-height: var(--radio-max-height);
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.m-radio-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:not(.radio-disabled):hover {
|
||||||
|
.radio-handle {
|
||||||
|
border-color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-handle {
|
||||||
|
/*
|
||||||
|
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小
|
||||||
|
如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
|
||||||
|
*/
|
||||||
|
flex-shrink: 0; // 默认 1.即空间不足时,项目将缩小
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-top: -8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-checked {
|
||||||
|
border-color: @themeColor;
|
||||||
|
background-color: @themeColor;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: scale(0.375);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 0 8px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.radio-handle {
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: scale(0.5);
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-radio-button-wrap {
|
||||||
|
position: relative;
|
||||||
|
height: 32px;
|
||||||
|
padding-inline: 15px;
|
||||||
|
line-height: 30px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right-width: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s,
|
||||||
|
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-left: 1px solid #d9d9d9;
|
||||||
|
border-start-start-radius: 6px;
|
||||||
|
border-end-start-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:first-child)::before {
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
display: block;
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
padding-block: 1px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
background-color: #d9d9d9;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-start-end-radius: 6px;
|
||||||
|
border-end-end-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.radio-button-disabled):hover {
|
||||||
|
color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button-checked:not(.radio-button-disabled) {
|
||||||
|
z-index: 1;
|
||||||
|
color: @themeColor;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-color: @themeColor;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button-disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button-disabled.radio-button-checked {
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button-solid {
|
||||||
|
.radio-button-checked:not(.radio-button-disabled) {
|
||||||
|
color: #fff;
|
||||||
|
background-color: @themeColor;
|
||||||
|
border-color: @themeColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button-small {
|
||||||
|
.m-radio-button-wrap {
|
||||||
|
height: 24px;
|
||||||
|
padding-inline: 7px;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-start-start-radius: 4px;
|
||||||
|
border-end-start-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-start-end-radius: 4px;
|
||||||
|
border-end-end-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button-large {
|
||||||
|
.m-radio-button-wrap {
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 38px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-start-start-radius: 8px;
|
||||||
|
border-end-start-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-start-end-radius: 8px;
|
||||||
|
border-end-end-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
391
src/components/MyUI/Rate/Rate.vue
Normal file
391
src/components/MyUI/Rate/Rate.vue
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, watch} from 'vue';
|
||||||
|
import Tooltip from '../Tooltip/Tooltip.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
allowClear?: boolean // 是否允许再次点击后清除
|
||||||
|
allowHalf?: boolean // 是否允许半选
|
||||||
|
count?: number // star 总数
|
||||||
|
character?: 'star-outlined' | 'star-filled' | 'heart-outlined' | 'heart-filled' | string // 自定义字符,预置四种图标 string | slot
|
||||||
|
size?: number // 字符大小,单位 px
|
||||||
|
color?: string // 字符选中颜色
|
||||||
|
gap?: number // 字符间距,单位 px
|
||||||
|
disabled?: boolean // 只读,无法进行交互
|
||||||
|
tooltips?: string[] // 自定义每项的提示信息
|
||||||
|
tooltipProps?: object // Tooltip 组件属性配置,参考 Tooltip Props
|
||||||
|
value?: number // (v-model) 当前数,受控值 0,1,2,3...
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
allowClear: true,
|
||||||
|
allowHalf: false,
|
||||||
|
count: 5,
|
||||||
|
character: 'star-filled',
|
||||||
|
size: 20,
|
||||||
|
color: '#fadb14',
|
||||||
|
gap: 8,
|
||||||
|
disabled: false,
|
||||||
|
tooltips: () => [],
|
||||||
|
tooltipProps: () => ({}),
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
const activeValue = ref();
|
||||||
|
const hoverValue = ref();
|
||||||
|
const tempValue = ref(); // 清除时保存点击value
|
||||||
|
const emits = defineEmits(['update:value', 'change', 'hoverChange']);
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(to) => {
|
||||||
|
activeValue.value = to;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
activeValue,
|
||||||
|
(to) => {
|
||||||
|
hoverValue.value = to;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function onClick(value: number) {
|
||||||
|
tempValue.value = null;
|
||||||
|
if (value !== activeValue.value) {
|
||||||
|
activeValue.value = value;
|
||||||
|
emits('change', value); // 选择时的回调
|
||||||
|
emits('update:value', value);
|
||||||
|
} else {
|
||||||
|
if (props.allowClear) {
|
||||||
|
tempValue.value = value;
|
||||||
|
activeValue.value = 0;
|
||||||
|
emits('change', 0);
|
||||||
|
emits('update:value', 0);
|
||||||
|
} else {
|
||||||
|
// 不允许清除
|
||||||
|
emits('change', value); // 选择时的回调
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFirstEnter(value: number) {
|
||||||
|
hoverValue.value = value;
|
||||||
|
emits('hoverChange', value); // 鼠标经过时数值变化的回调
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSecondEnter(value: number) {
|
||||||
|
hoverValue.value = value;
|
||||||
|
emits('hoverChange', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTempValue() {
|
||||||
|
// 重置点击 value
|
||||||
|
tempValue.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeave() {
|
||||||
|
hoverValue.value = activeValue.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUp() {
|
||||||
|
tempValue.value = null;
|
||||||
|
if (activeValue.value < props.count) {
|
||||||
|
activeValue.value += (props.allowHalf ? 0.5 : 1);
|
||||||
|
emits('change', activeValue.value);
|
||||||
|
emits('update:value', activeValue.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDown() {
|
||||||
|
tempValue.value = null;
|
||||||
|
if (activeValue.value > 0) {
|
||||||
|
activeValue.value -= (props.allowHalf ? 0.5 : 1);
|
||||||
|
emits('change', activeValue.value);
|
||||||
|
emits('update:value', activeValue.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-rate"
|
||||||
|
:class="{ 'rate-disabled': disabled }"
|
||||||
|
:style="`--star-color: ${color}; --star-gap: ${gap}px; --star-size: ${size}px;`"
|
||||||
|
@mouseleave="onLeave"
|
||||||
|
>
|
||||||
|
<template v-for="n in count" :key="n">
|
||||||
|
<Tooltip v-bind="tooltipProps">
|
||||||
|
<template v-if="tooltips[n - 1]" #tooltip>
|
||||||
|
<slot name="tooltip" :value="n">{{ tooltips[n - 1] }}</slot>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="rate-star"
|
||||||
|
:class="{
|
||||||
|
'star-half': allowHalf && hoverValue >= n - 0.5 && hoverValue < n,
|
||||||
|
'star-full': hoverValue >= n,
|
||||||
|
'temp-gray': !allowHalf && tempValue === n
|
||||||
|
}"
|
||||||
|
@click="allowHalf ? () => false : onClick(n)"
|
||||||
|
@keydown.prevent.right="onUp"
|
||||||
|
@keydown.prevent.left="onDown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="allowHalf"
|
||||||
|
class="star-first"
|
||||||
|
:class="{ 'temp-gray-first': tempValue === n - 0.5 }"
|
||||||
|
@click.stop="onClick(n - 0.5)"
|
||||||
|
@mouseenter="onFirstEnter(n - 0.5)"
|
||||||
|
@mouseleave="resetTempValue"
|
||||||
|
>
|
||||||
|
<slot name="character">
|
||||||
|
<svg
|
||||||
|
v-if="character === 'star-filled'"
|
||||||
|
class="icon-character"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="star"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M908.1 353.1l-253.9-36.9L540.7 86.1c-3.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L369.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3a32.05 32.05 0 00.6 45.3l183.7 179.1-43.4 252.9a31.95 31.95 0 0046.4 33.7L512 754l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="character === 'star-outlined'"
|
||||||
|
class="icon-character"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="star"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M908.1 353.1l-253.9-36.9L540.7 86.1c-3.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L369.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3a32.05 32.05 0 00.6 45.3l183.7 179.1-43.4 252.9a31.95 31.95 0 0046.4 33.7L512 754l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3zM664.8 561.6l36.1 210.3L512 672.7 323.1 772l36.1-210.3-152.8-149L417.6 382 512 190.7 606.4 382l211.2 30.7-152.8 148.9z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="character === 'heart-filled'"
|
||||||
|
class="icon-character"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="heart"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M923 283.6a260.04 260.04 0 00-56.9-82.8 264.4 264.4 0 00-84-55.5A265.34 265.34 0 00679.7 125c-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5a258.44 258.44 0 00-56.9 82.8c-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3.1-35.3-7-69.6-20.9-101.9z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="character === 'heart-outlined'"
|
||||||
|
class="icon-character"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="heart"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M923 283.6a260.04 260.04 0 00-56.9-82.8 264.4 264.4 0 00-84-55.5A265.34 265.34 0 00679.7 125c-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5a258.44 258.44 0 00-56.9 82.8c-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3.1-35.3-7-69.6-20.9-101.9zM512 814.8S156 586.7 156 385.5C156 283.6 240.3 201 344.3 201c73.1 0 136.5 40.8 167.7 100.4C543.2 241.8 606.6 201 679.7 201c104 0 188.3 82.6 188.3 184.5 0 201.2-356 429.3-356 429.3z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span v-else-if="character" class="icon-character">
|
||||||
|
{{ character }}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="star-second"
|
||||||
|
:class="{ 'temp-gray-second': tempValue === n }"
|
||||||
|
@click.stop="onClick(n)"
|
||||||
|
@mouseenter="onSecondEnter(n)"
|
||||||
|
@mouseleave="resetTempValue"
|
||||||
|
>
|
||||||
|
<slot name="character">
|
||||||
|
<svg
|
||||||
|
v-if="character === 'star-filled'"
|
||||||
|
class="icon-character"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="star"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M908.1 353.1l-253.9-36.9L540.7 86.1c-3.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L369.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3a32.05 32.05 0 00.6 45.3l183.7 179.1-43.4 252.9a31.95 31.95 0 0046.4 33.7L512 754l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="character === 'star-outlined'"
|
||||||
|
class="icon-character"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="star"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M908.1 353.1l-253.9-36.9L540.7 86.1c-3.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L369.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3a32.05 32.05 0 00.6 45.3l183.7 179.1-43.4 252.9a31.95 31.95 0 0046.4 33.7L512 754l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3zM664.8 561.6l36.1 210.3L512 672.7 323.1 772l36.1-210.3-152.8-149L417.6 382 512 190.7 606.4 382l211.2 30.7-152.8 148.9z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="character === 'heart-filled'"
|
||||||
|
class="icon-character"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="heart"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M923 283.6a260.04 260.04 0 00-56.9-82.8 264.4 264.4 0 00-84-55.5A265.34 265.34 0 00679.7 125c-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5a258.44 258.44 0 00-56.9 82.8c-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3.1-35.3-7-69.6-20.9-101.9z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="character === 'heart-outlined'"
|
||||||
|
class="icon-character"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="heart"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M923 283.6a260.04 260.04 0 00-56.9-82.8 264.4 264.4 0 00-84-55.5A265.34 265.34 0 00679.7 125c-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5a258.44 258.44 0 00-56.9 82.8c-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3.1-35.3-7-69.6-20.9-101.9zM512 814.8S156 586.7 156 385.5C156 283.6 240.3 201 344.3 201c73.1 0 136.5 40.8 167.7 100.4C543.2 241.8 606.6 201 679.7 201c104 0 188.3 82.6 188.3 184.5 0 201.2-356 429.3-356 429.3z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span v-else class="icon-character">
|
||||||
|
{{ character }}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-rate {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--star-gap);
|
||||||
|
line-height: normal;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.rate-star {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 1px dashed var(--star-color);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
font-size: var(--star-size);
|
||||||
|
color: rgba(0, 0, 0, 0.06);
|
||||||
|
fill: currentColor;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-character {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 1em;
|
||||||
|
font-size: var(--star-size);
|
||||||
|
color: rgba(0, 0, 0, 0.06);
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-first {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-second {
|
||||||
|
display: inline-block;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-gray-first {
|
||||||
|
&:hover {
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
.icon-character {
|
||||||
|
color: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-gray-second {
|
||||||
|
&:hover {
|
||||||
|
.icon-character {
|
||||||
|
color: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-half {
|
||||||
|
.star-first {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: var(--star-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-character {
|
||||||
|
color: var(--star-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-full {
|
||||||
|
.star-second {
|
||||||
|
:deep(svg) {
|
||||||
|
color: var(--star-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-character {
|
||||||
|
color: var(--star-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1018
src/components/MyUI/Result/Result.vue
Normal file
1018
src/components/MyUI/Result/Result.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {CSSProperties} from 'vue';
|
import type {CSSProperties} from 'vue';
|
||||||
import {computed, onMounted, ref} from 'vue';
|
import {computed, onMounted, ref} from 'vue';
|
||||||
import {debounce, useEventListener, useMutationObserver} from '../utils';
|
import {debounce, useEventListener, useMutationObserver} from '../Utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
contentClass?: string // 内容 div 的类名
|
contentClass?: string // 内容 div 的类名
|
||||||
|
|||||||
220
src/components/MyUI/Segmented/Segmented.vue
Normal file
220
src/components/MyUI/Segmented/Segmented.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface SegmentedOption {
|
||||||
|
label?: string // 选项名
|
||||||
|
value: string | number // 选项值
|
||||||
|
disabled?: boolean // 是否禁用选项
|
||||||
|
payload?: any // 自定义数据载体
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
block?: boolean // 是否将宽度调整为父元素宽度,同时所有选项占据相同的宽度
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
options?: string[] | number[] | SegmentedOption[] // 选项数据
|
||||||
|
size?: 'small' | 'middle' | 'large' // 控件尺寸
|
||||||
|
value?: string | number // (v-model) 当前选中的值
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
block: false,
|
||||||
|
disabled: false,
|
||||||
|
options: () => [],
|
||||||
|
size: 'middle',
|
||||||
|
value: undefined
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['update:value', 'change']);
|
||||||
|
|
||||||
|
function onSelected(value: string | number) {
|
||||||
|
if (value !== props.value) {
|
||||||
|
emits('update:value', value);
|
||||||
|
emits('change', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionDisabled(option: string | number | SegmentedOption) {
|
||||||
|
if (typeof option == 'object') {
|
||||||
|
return option?.disabled || false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionValue(option: string | number | SegmentedOption) {
|
||||||
|
if (typeof option == 'object') {
|
||||||
|
return option.value;
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionLabel(option: string | number | SegmentedOption) {
|
||||||
|
if (typeof option == 'object') {
|
||||||
|
return option.label;
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-segmented"
|
||||||
|
:class="{
|
||||||
|
'segmented-small': size == 'small',
|
||||||
|
'segmented-large': size == 'large',
|
||||||
|
'segmented-block': block
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="m-segmented-group">
|
||||||
|
<div
|
||||||
|
class="m-segmented-item"
|
||||||
|
:class="{
|
||||||
|
'segmented-item-selected': value === getOptionValue(option),
|
||||||
|
'segmented-item-disabled': disabled || getOptionDisabled(option),
|
||||||
|
'segmented-item-block': block
|
||||||
|
}"
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="index"
|
||||||
|
@click="disabled || getOptionDisabled(option) ? () => false : onSelected(getOptionValue(option))"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="segmented-item-input"
|
||||||
|
:checked="value === getOptionValue(option)"
|
||||||
|
:disabled="disabled || getOptionDisabled(option)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="segmented-item-label"
|
||||||
|
:title="typeof option === 'object' && option.payload ? undefined : String(getOptionLabel(option))"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="label"
|
||||||
|
:label="getOptionLabel(option)"
|
||||||
|
:payload="typeof option === 'object' ? option.payload : {}"
|
||||||
|
>
|
||||||
|
{{ getOptionLabel(option) }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-segmented {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
|
||||||
|
.m-segmented-group {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.m-segmented-item {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||||
|
background-color 0.2s;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover:not(.segmented-item-selected):not(.segmented-item-disabled) {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-item-input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-item-label {
|
||||||
|
min-height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
padding: 0 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-item-selected {
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03),
|
||||||
|
0 1px 6px -1px rgba(0, 0, 0, 0.02),
|
||||||
|
0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-item-disabled {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-small {
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.m-segmented-group .m-segmented-item {
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
.segmented-item-label {
|
||||||
|
min-height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 0 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-large {
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.m-segmented-group .m-segmented-item {
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.segmented-item-label {
|
||||||
|
min-height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
padding: 0 11px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-block {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.m-segmented-group .m-segmented-item {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
547
src/components/MyUI/Select/Select.vue
Normal file
547
src/components/MyUI/Select/Select.vue
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watchEffect, watch } from 'vue';
|
||||||
|
import Empty from '../Empty/Empty.vue';
|
||||||
|
import Scrollbar from '../Scrollbar/Scrollbar.vue';
|
||||||
|
interface Option {
|
||||||
|
label?: string // 选项名
|
||||||
|
value?: string | number // 选项值
|
||||||
|
disabled?: boolean // 是否禁用选项,默认 false
|
||||||
|
[propName: string]: any // 添加一个字符串索引签名,用于包含带有任意数量的其他属性
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
options?: Option[] // 选项数据
|
||||||
|
label?: string // 字典项的文本字段名
|
||||||
|
value?: string // 字典项的值字段名
|
||||||
|
placeholder?: string // 默认占位文本
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
allowClear?: boolean // 是否支持清除
|
||||||
|
search?: boolean // 是否支持搜索,使用搜索时请设置 width
|
||||||
|
/*
|
||||||
|
根据输入项进行筛选,默认为 true 时,筛选每个选项的文本字段 label 是否包含输入项,包含返回 true,反之返回 false
|
||||||
|
当其为函数 Function 时,接受 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false
|
||||||
|
*/
|
||||||
|
filter?: any | true // 过滤条件函数,仅当支持搜索时生效
|
||||||
|
width?: string | number // 选择器宽度,单位 px
|
||||||
|
height?: number // 选择器高度,单位 px
|
||||||
|
maxDisplay?: number // 下拉面板最多能展示的下拉项数,超过后滚动显示
|
||||||
|
scrollbarProps?: object // 下拉面板滚动条 scrollbar 组件属性配置
|
||||||
|
modelValue?: number | string // (v-model) 当前选中的 option 条目值
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
options: () => [],
|
||||||
|
label: 'label',
|
||||||
|
value: 'value',
|
||||||
|
placeholder: '请选择',
|
||||||
|
disabled: false,
|
||||||
|
search: false,
|
||||||
|
allowClear: false,
|
||||||
|
filter: true,
|
||||||
|
width: 'auto',
|
||||||
|
height: 32,
|
||||||
|
maxDisplay: 6,
|
||||||
|
scrollbarProps: () => ({}),
|
||||||
|
modelValue: undefined
|
||||||
|
});
|
||||||
|
const filterOptions = ref<Option[]>(); // 过滤后的选项数组
|
||||||
|
const selectedName = ref(); // 当前选中选项的 label
|
||||||
|
const inputRef = ref(); // 输入框 DOM 引用
|
||||||
|
const inputValue = ref(); // 支持搜索时,用户输入内容
|
||||||
|
const disabledBlur = ref(false); // 是否禁用 input 标签的 blur 事件
|
||||||
|
const hideSelectName = ref(false); // 用户输入时,隐藏 selectName 的展示
|
||||||
|
const hoverValue = ref(); // 鼠标悬浮项的 value 值
|
||||||
|
const showOptions = ref(false); // options 面板
|
||||||
|
const showArrow = ref(true); // 剪头图标显隐
|
||||||
|
const showClear = ref(false); // 清除图标显隐
|
||||||
|
const showCaret = ref(false); // 支持搜索时,输入光标的显隐
|
||||||
|
const showSearch = ref(false); // 搜索图标显隐
|
||||||
|
const selectFocused = ref(false); /// select 是否聚焦
|
||||||
|
const emits = defineEmits(['update:modelValue', 'change', 'openChange']);
|
||||||
|
const selectWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const optionsStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
maxHeight: `${props.maxDisplay * props.height + 8}px`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.search) {
|
||||||
|
if (inputValue.value) {
|
||||||
|
showOptions.value = true;
|
||||||
|
filterOptions.value = props.options.filter((option) => {
|
||||||
|
if (typeof props.filter === 'function') {
|
||||||
|
return props.filter(inputValue.value, option);
|
||||||
|
} else {
|
||||||
|
return option[props.label].includes(inputValue.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
filterOptions.value = [...props.options];
|
||||||
|
}
|
||||||
|
if (filterOptions.value.length && inputValue.value) {
|
||||||
|
hoverValue.value = filterOptions.value[0][props.value];
|
||||||
|
} else {
|
||||||
|
hoverValue.value = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filterOptions.value = props.options;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
// 回调立即执行一次,同时会自动跟踪回调中所依赖的所有响应式依赖
|
||||||
|
initSelector();
|
||||||
|
});
|
||||||
|
watch(showOptions, (to) => {
|
||||||
|
emits('openChange', to);
|
||||||
|
if (props.search && !to) {
|
||||||
|
inputValue.value = undefined;
|
||||||
|
hideSelectName.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function initSelector() {
|
||||||
|
if (props.modelValue) {
|
||||||
|
const target = props.options.find((option) => option[props.value] === props.modelValue);
|
||||||
|
if (target) {
|
||||||
|
selectedName.value = target[props.label];
|
||||||
|
hoverValue.value = target[props.value];
|
||||||
|
} else {
|
||||||
|
selectedName.value = props.modelValue;
|
||||||
|
hoverValue.value = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedName.value = null;
|
||||||
|
hoverValue.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onBlur() {
|
||||||
|
selectFocused.value = false;
|
||||||
|
if (showOptions.value) {
|
||||||
|
showOptions.value = false;
|
||||||
|
}
|
||||||
|
if (props.search) {
|
||||||
|
showSearch.value = false;
|
||||||
|
showArrow.value = true;
|
||||||
|
hideSelectName.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onEnter() {
|
||||||
|
disabledBlur.value = true;
|
||||||
|
if (props.allowClear) {
|
||||||
|
if (selectedName.value || (props.search && inputValue.value)) {
|
||||||
|
showArrow.value = false;
|
||||||
|
showClear.value = true;
|
||||||
|
if (props.search) {
|
||||||
|
showSearch.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onLeave() {
|
||||||
|
disabledBlur.value = false;
|
||||||
|
if (props.allowClear && showClear.value) {
|
||||||
|
showClear.value = false;
|
||||||
|
if (!props.search) {
|
||||||
|
showArrow.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.search) {
|
||||||
|
if (showOptions.value) {
|
||||||
|
showSearch.value = true;
|
||||||
|
showArrow.value = false;
|
||||||
|
} else {
|
||||||
|
showSearch.value = false;
|
||||||
|
showArrow.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onHover(value: string | number, disabled: boolean | undefined) {
|
||||||
|
disabledBlur.value = Boolean(disabled);
|
||||||
|
hoverValue.value = value;
|
||||||
|
}
|
||||||
|
function openSelect() {
|
||||||
|
selectFocus();
|
||||||
|
if (!props.search) {
|
||||||
|
inputRef.value.style.opacity = 0;
|
||||||
|
}
|
||||||
|
showOptions.value = !showOptions.value;
|
||||||
|
if (!hoverValue.value && selectedName.value) {
|
||||||
|
const target = props.options.find((option) => option[props.label] === selectedName.value);
|
||||||
|
hoverValue.value = target ? target[props.value] : null;
|
||||||
|
}
|
||||||
|
if (props.search) {
|
||||||
|
if (!showClear.value) {
|
||||||
|
showArrow.value = !showOptions.value;
|
||||||
|
showSearch.value = showOptions.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onSearchInput(e: InputEvent) {
|
||||||
|
hideSelectName.value = Boolean((e.target as HTMLInputElement)?.value);
|
||||||
|
}
|
||||||
|
function onClear() {
|
||||||
|
if (selectFocused.value) {
|
||||||
|
selectFocus();
|
||||||
|
showCaret.value = true;
|
||||||
|
}
|
||||||
|
showClear.value = false;
|
||||||
|
selectedName.value = null;
|
||||||
|
hoverValue.value = null;
|
||||||
|
showOptions.value = false;
|
||||||
|
showSearch.value = false;
|
||||||
|
showArrow.value = true;
|
||||||
|
emits('update:modelValue');
|
||||||
|
emits('change');
|
||||||
|
}
|
||||||
|
function selectFocus() {
|
||||||
|
inputRef.value.focus(); // 通过 input 标签聚焦来模拟 select 整体聚焦效果
|
||||||
|
selectFocused.value = true;
|
||||||
|
}
|
||||||
|
function onChange(value: string | number, label: string, index: number) {
|
||||||
|
// 选中下拉项后的回调
|
||||||
|
if (props.modelValue !== value) {
|
||||||
|
selectedName.value = label;
|
||||||
|
hoverValue.value = value;
|
||||||
|
emits('update:modelValue', value);
|
||||||
|
emits('change', value, label, index);
|
||||||
|
}
|
||||||
|
showCaret.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-select"
|
||||||
|
:class="{ 'select-focused': selectFocused, 'search-select': search, 'select-disabled': disabled }"
|
||||||
|
:style="`width: ${selectWidth}; height: ${height}px;`"
|
||||||
|
@click="disabled ? () => false : openSelect()"
|
||||||
|
>
|
||||||
|
<div class="m-select-wrap" @mouseenter="onEnter" @mouseleave="onLeave">
|
||||||
|
<span class="m-select-search">
|
||||||
|
<Input
|
||||||
|
ref="inputRef"
|
||||||
|
class="select-search"
|
||||||
|
:class="{ 'caret-show': showOptions || showCaret }"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
:readonly="!search"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="onSearchInput"
|
||||||
|
v-model="inputValue"
|
||||||
|
@blur="!disabledBlur && showOptions && !disabled ? onBlur() : () => false"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!hideSelectName"
|
||||||
|
class="select-item"
|
||||||
|
:class="{ 'select-placeholder': !selectedName || showOptions }"
|
||||||
|
:style="`line-height: ${height - 2}px;`"
|
||||||
|
:title="selectedName"
|
||||||
|
>
|
||||||
|
{{ selectedName || placeholder }}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="arrow-svg"
|
||||||
|
:class="{ 'arrow-rotate': showOptions, 'show-svg': showArrow }"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="down"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="search-svg"
|
||||||
|
:class="{ 'show-svg': showSearch }"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="search"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="clear-svg"
|
||||||
|
:class="{ 'show-svg': showClear }"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="close-circle"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
@click.stop="onClear"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<Transition name="slide-up">
|
||||||
|
<div
|
||||||
|
v-if="showOptions && filterOptions && filterOptions.length"
|
||||||
|
class="m-options-panel"
|
||||||
|
:style="`top: ${height + 4}px;`"
|
||||||
|
@mouseleave="disabledBlur = false"
|
||||||
|
>
|
||||||
|
<Scrollbar :content-style="{ padding: '4px' }" :style="optionsStyle" v-bind="scrollbarProps">
|
||||||
|
<p
|
||||||
|
v-for="(option, index) in filterOptions"
|
||||||
|
:key="index"
|
||||||
|
:class="[
|
||||||
|
'select-option',
|
||||||
|
{
|
||||||
|
'option-hover': !option.disabled && option[value] === hoverValue,
|
||||||
|
'option-selected': option[label] === selectedName,
|
||||||
|
'option-disabled': option.disabled
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:title="option[label]"
|
||||||
|
@mouseenter="onHover(option[value], option.disabled)"
|
||||||
|
@click.stop="option.disabled ? selectFocus() : onChange(option[value], option[label], index)"
|
||||||
|
>
|
||||||
|
{{ option[label] }}
|
||||||
|
</p>
|
||||||
|
</Scrollbar>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="showOptions && filterOptions && !filterOptions.length"
|
||||||
|
class="options-empty"
|
||||||
|
:style="`top: ${height + 4}px; width: ${width}px;`"
|
||||||
|
>
|
||||||
|
<Empty image="outlined" />
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.slide-up-enter-active {
|
||||||
|
transform: scaleY(1);
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.2s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
.slide-up-leave-active {
|
||||||
|
transform: scaleY(1);
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.2s cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
||||||
|
}
|
||||||
|
.slide-up-enter-from,
|
||||||
|
.slide-up-leave-to {
|
||||||
|
transform: scaleY(0.8);
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.m-select {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
&:not(.select-disabled):hover {
|
||||||
|
// 悬浮时样式
|
||||||
|
.m-select-wrap {
|
||||||
|
border-color: #4096ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.m-select-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 11px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
.m-select-search {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 11px;
|
||||||
|
right: 11px;
|
||||||
|
.select-search {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
caret-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
appearance: none;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.caret-show {
|
||||||
|
caret-color: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select-item {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 18px;
|
||||||
|
flex: 1;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.select-placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
transition: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.icon-svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 11px;
|
||||||
|
margin: auto 0;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
fill: currentColor;
|
||||||
|
opacity: 0;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.arrow-svg {
|
||||||
|
.icon-svg();
|
||||||
|
transition:
|
||||||
|
transform 0.3s,
|
||||||
|
opacity 0.3s;
|
||||||
|
}
|
||||||
|
.arrow-rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.search-svg {
|
||||||
|
.icon-svg();
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.clear-svg {
|
||||||
|
.icon-svg();
|
||||||
|
z-index: 1;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
opacity 0.3s;
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show-svg {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.m-options-panel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
outline: none;
|
||||||
|
box-shadow:
|
||||||
|
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
.select-option {
|
||||||
|
// 下拉项默认样式
|
||||||
|
min-height: 32px;
|
||||||
|
display: block;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.option-hover {
|
||||||
|
// 悬浮时的下拉项样式
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.option-selected {
|
||||||
|
// 被选中的下拉项样式
|
||||||
|
font-weight: 600;
|
||||||
|
background: #e6f4ff;
|
||||||
|
}
|
||||||
|
.option-disabled {
|
||||||
|
// 禁用某个下拉选项时的样式
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.options-empty {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
outline: none;
|
||||||
|
box-shadow:
|
||||||
|
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
.m-empty {
|
||||||
|
margin-block: 8px;
|
||||||
|
:deep(.m-empty-image) {
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select-focused:not(.select-disabled) {
|
||||||
|
// 激活时样式
|
||||||
|
.m-select-wrap {
|
||||||
|
border-color: #4096ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search-select {
|
||||||
|
.m-select-wrap {
|
||||||
|
cursor: text;
|
||||||
|
.m-select-search {
|
||||||
|
.select-search {
|
||||||
|
cursor: auto;
|
||||||
|
color: inherit;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select-disabled {
|
||||||
|
.m-select-wrap {
|
||||||
|
// 下拉禁用样式
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
background: #f5f5f5;
|
||||||
|
user-select: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
.m-select-search .select-search {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
352
src/components/MyUI/Skeleton/Skeleton.vue
Normal file
352
src/components/MyUI/Skeleton/Skeleton.vue
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
interface SkeletonButtonProps {
|
||||||
|
shape?: 'default' | 'round' | 'circle' // 指定按钮的形状,默认 'default'
|
||||||
|
size?: 'small' | 'middle' | 'large' // 设置按钮的大小,默认 'middle'
|
||||||
|
block?: boolean // 将按钮宽度调整为其父宽度的选项,默认 false
|
||||||
|
}
|
||||||
|
interface SkeletonAvatarProps {
|
||||||
|
shape?: 'circle' | 'square' // 指定头像的形状,默认 'circle'
|
||||||
|
size?: number | 'small' | 'middle' | 'large' // 设置头像占位图的大小,默认 'middle'
|
||||||
|
}
|
||||||
|
interface SkeletonInputProps {
|
||||||
|
size: 'small' | 'middle' | 'large' // 设置输入框的大小,默认 'middle'
|
||||||
|
}
|
||||||
|
interface SkeletonTitleProps {
|
||||||
|
width?: number | string // 设置标题占位图的宽度,默认 '38%'
|
||||||
|
}
|
||||||
|
interface SkeletonParagraphProps {
|
||||||
|
rows?: number | string // 设置段落占位图的行数,默认 avatar ? 2 : 3
|
||||||
|
width?: number | string | Array<number | string> // 设置段落占位图的宽度,若为数组时则为对应的每行宽度,反之则是最后一行的宽度,默认 '61%'
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
animated?: boolean // 是否展示动画效果
|
||||||
|
button?: boolean | SkeletonButtonProps // 是否使用按钮占位图
|
||||||
|
avatar?: boolean | SkeletonAvatarProps // 是否显示头像占位图
|
||||||
|
input?: boolean | SkeletonInputProps // 是否使用输入框占位图
|
||||||
|
image?: boolean // 是否使用图像占位图
|
||||||
|
title?: boolean | SkeletonTitleProps // 是否显示标题占位图
|
||||||
|
paragraph?: boolean | SkeletonParagraphProps // 是否显示段落占位图
|
||||||
|
loading?: boolean // 为 true 时,显示占位图,反之则直接展示子组件
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
animated: true,
|
||||||
|
button: false,
|
||||||
|
image: false,
|
||||||
|
avatar: false,
|
||||||
|
input: false,
|
||||||
|
title: true,
|
||||||
|
paragraph: true,
|
||||||
|
loading: true
|
||||||
|
});
|
||||||
|
const buttonSize = computed(() => {
|
||||||
|
if (typeof props.button === 'object') {
|
||||||
|
if (props.button.size === 'large') {
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
if (props.button.size === 'small') {
|
||||||
|
return 24;
|
||||||
|
}
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
return 32;
|
||||||
|
});
|
||||||
|
const titleTop = computed(() => {
|
||||||
|
if (typeof props.avatar === 'boolean') {
|
||||||
|
return 8;
|
||||||
|
} else {
|
||||||
|
if (typeof props.avatar.size === 'number') {
|
||||||
|
return (props.avatar.size - 16) / 2;
|
||||||
|
} else {
|
||||||
|
const topMap = {
|
||||||
|
small: 4,
|
||||||
|
middle: 8,
|
||||||
|
large: 12
|
||||||
|
};
|
||||||
|
return topMap[props.avatar.size || 'middle'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const titleWidth = computed(() => {
|
||||||
|
if (typeof props.title === 'boolean') {
|
||||||
|
return '38%';
|
||||||
|
} else {
|
||||||
|
if (typeof props.title.width === 'number') {
|
||||||
|
return `${props.title.width}px`;
|
||||||
|
}
|
||||||
|
return props.title.width || '38%';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const paragraphRows = computed(() => {
|
||||||
|
if (typeof props.paragraph === 'boolean') {
|
||||||
|
if (props.avatar) {
|
||||||
|
return 2;
|
||||||
|
} else {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (props.avatar) {
|
||||||
|
return props.paragraph.rows || 2;
|
||||||
|
} else {
|
||||||
|
return props.paragraph.rows || 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const paragraphWidth = computed(() => {
|
||||||
|
if (typeof props.paragraph === 'object') {
|
||||||
|
if (Array.isArray(props.paragraph.width)) {
|
||||||
|
return props.paragraph.width.map((width: number | string) => {
|
||||||
|
if (typeof width === 'number') {
|
||||||
|
return `${width}px`;
|
||||||
|
} else {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (typeof props.paragraph.width === 'number') {
|
||||||
|
return Array(paragraphRows.value).fill(`${props.paragraph.width}px`);
|
||||||
|
} else if (typeof props.paragraph.width === 'string') {
|
||||||
|
return Array(paragraphRows.value).fill(props.paragraph.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array(paragraphRows.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="m-skeleton"
|
||||||
|
:class="{ 'skeleton-avatar': avatar, 'skeleton-animated': animated }"
|
||||||
|
:style="`--button-size: ${buttonSize}px; --title-top: ${titleTop}px;`"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="button"
|
||||||
|
class="skeleton-button"
|
||||||
|
:class="{
|
||||||
|
'button-round': typeof button !== 'boolean' && button.shape === 'round',
|
||||||
|
'button-circle': typeof button !== 'boolean' && button.shape === 'circle',
|
||||||
|
'button-sm': typeof button !== 'boolean' && button.size === 'small',
|
||||||
|
'button-lg': typeof button !== 'boolean' && button.size === 'large',
|
||||||
|
'button-block': typeof button !== 'boolean' && button.shape !== 'circle' && button.block
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-if="input"
|
||||||
|
class="skeleton-input"
|
||||||
|
:class="{
|
||||||
|
'input-sm': typeof input !== 'boolean' && input.size === 'small',
|
||||||
|
'input-lg': typeof input !== 'boolean' && input.size === 'large'
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<div v-if="image" class="skeleton-image">
|
||||||
|
<svg class="image-svg" viewBox="0 0 1098 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
class="svg-path"
|
||||||
|
d="M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div v-if="avatar" class="skeleton-header">
|
||||||
|
<span
|
||||||
|
class="skeleton-avatar"
|
||||||
|
:class="{
|
||||||
|
'avatar-sm': typeof avatar !== 'boolean' && avatar.size === 'small',
|
||||||
|
'avatar-lg': typeof avatar !== 'boolean' && avatar.size === 'large',
|
||||||
|
'avatar-square': typeof avatar !== 'boolean' && avatar.shape === 'square'
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<template v-if="!button && !image && !input">
|
||||||
|
<div v-if="title || paragraph" class="skeleton-content">
|
||||||
|
<h3 v-if="title" class="skeleton-title" :style="{ width: titleWidth }"></h3>
|
||||||
|
<ul v-if="paragraph" class="skeleton-paragraph" :class="{ mt24: title, mt28: title && avatar }">
|
||||||
|
<li v-for="n in paragraphRows" :key="n" :style="`width: ${paragraphWidth[(n as number) - 1]};`"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<slot v-else></slot>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-skeleton {
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
.skeleton-button {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 64px;
|
||||||
|
min-width: 64px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.button-sm {
|
||||||
|
width: 48px;
|
||||||
|
min-width: 48px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.button-lg {
|
||||||
|
width: 80px;
|
||||||
|
min-width: 80px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
.button-round {
|
||||||
|
border-radius: var(--button-size);
|
||||||
|
}
|
||||||
|
.button-circle {
|
||||||
|
width: var(--button-size);
|
||||||
|
min-width: var(--button-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.button-block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.skeleton-input {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 160px;
|
||||||
|
min-width: 160px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.input-sm {
|
||||||
|
width: 120px;
|
||||||
|
min-width: 120px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.input-lg {
|
||||||
|
width: 200px;
|
||||||
|
min-width: 200px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
.skeleton-image {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
vertical-align: top;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
line-height: 96px;
|
||||||
|
.image-svg {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
max-width: 192px;
|
||||||
|
max-height: 192px;
|
||||||
|
color: #bfbfbf;
|
||||||
|
.svg-path {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.skeleton-header {
|
||||||
|
display: table-cell;
|
||||||
|
padding-right: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
.skeleton-avatar {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.avatar-sm {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.avatar-lg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
.avatar-square {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.skeleton-content {
|
||||||
|
display: table-cell;
|
||||||
|
width: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
.skeleton-title {
|
||||||
|
margin: 0;
|
||||||
|
height: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.skeleton-paragraph {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
li {
|
||||||
|
height: 16px;
|
||||||
|
list-style: none;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
width: 61%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mt24 {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.mt28 {
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.skeleton-avatar {
|
||||||
|
.skeleton-content {
|
||||||
|
.skeleton-title {
|
||||||
|
margin-top: var(--title-top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.skeleton-animated {
|
||||||
|
.skeleton-button,
|
||||||
|
.skeleton-input,
|
||||||
|
.skeleton-image,
|
||||||
|
.skeleton-header .skeleton-avatar,
|
||||||
|
.skeleton-content .skeleton-title,
|
||||||
|
.skeleton-content .skeleton-paragraph li {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -150%;
|
||||||
|
bottom: 0;
|
||||||
|
right: -150%;
|
||||||
|
background: linear-gradient(90deg, rgba(0, 0, 0, 0.06) 25%, rgba(0, 0, 0, 0.15) 37%, rgba(0, 0, 0, 0.06) 63%);
|
||||||
|
animation-name: skeleton-loading;
|
||||||
|
animation-duration: 1.4s;
|
||||||
|
animation-timing-function: ease;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-37.5%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(37.5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
712
src/components/MyUI/Slider/Slider.vue
Normal file
712
src/components/MyUI/Slider/Slider.vue
Normal file
@@ -0,0 +1,712 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, onMounted, ref, watch} from 'vue';
|
||||||
|
import {useResizeObserver} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 滑动输入条宽度,单位 px,水平模式时生效
|
||||||
|
height?: string | number // 滑动输入条高度,单位 px,垂直模式时生效
|
||||||
|
vertical?: boolean // 是否启用垂直模式
|
||||||
|
min?: number // 最小值
|
||||||
|
max?: number // 最大值
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
range?: boolean // 是否使用双滑块模式
|
||||||
|
step?: number // 步长,取值必须大于 0,并且可被 (max - min) 整除
|
||||||
|
formatTooltip?: (value: number) => string | number // Slider 会把当前值传给 formatTooltip,并在 Tooltip 中显示 formatTooltip 的返回值
|
||||||
|
tooltip?: boolean // 是否展示 Tooltip
|
||||||
|
value?: number | number[] // (v-model) 设置当前取值,当 range 为 false 时,使用 number,否则用 [number, number]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
vertical: false,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
disabled: false,
|
||||||
|
range: false,
|
||||||
|
step: 1,
|
||||||
|
formatTooltip: (value: number) => value,
|
||||||
|
tooltip: true,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
const sliderRef = ref(); // slider DOM 引用
|
||||||
|
const sliderWidth = ref(); // 滑动输入条宽度
|
||||||
|
const sliderHeight = ref(); // 滑动输入条高度
|
||||||
|
const low = ref(0); // 左/下滑块距离滑动条左/上端的距离
|
||||||
|
const high = ref(0); // 右/上滑动距离滑动条左/上端的距离
|
||||||
|
const lowHandle = ref(); // low handle 模板引用
|
||||||
|
const lowTooltip = ref(); // low tooltip 模板应用
|
||||||
|
const highHandle = ref(); // high handle 模板引用
|
||||||
|
const highTooltip = ref(); // high tooltip 模板引用
|
||||||
|
const emits = defineEmits(['update:value', 'change']);
|
||||||
|
const sliderSize = computed(() => {
|
||||||
|
if (!props.vertical) {
|
||||||
|
return sliderWidth.value;
|
||||||
|
} else {
|
||||||
|
return sliderHeight.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sliderStyle = computed(() => {
|
||||||
|
if (!props.vertical) {
|
||||||
|
return {
|
||||||
|
width: typeof props.width === 'number' ? `${props.width}px` : props.width
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
height: typeof props.height === 'number' ? `${props.height}px` : props.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const trackStyle = computed(() => {
|
||||||
|
if (!props.vertical) {
|
||||||
|
return {
|
||||||
|
left: `${low.value}px`,
|
||||||
|
right: 'auto',
|
||||||
|
width: `${high.value - low.value}px`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
bottom: `${low.value}px`,
|
||||||
|
top: 'auto',
|
||||||
|
height: `${high.value - low.value}px`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lowHandleStyle = computed(() => {
|
||||||
|
if (!props.vertical) {
|
||||||
|
return {
|
||||||
|
left: `${low.value}px`,
|
||||||
|
right: 'auto',
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
bottom: `${low.value}px`,
|
||||||
|
top: 'auto',
|
||||||
|
transform: 'translate(-50%, 50%)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const highHandleStyle = computed(() => {
|
||||||
|
if (!props.vertical) {
|
||||||
|
return {
|
||||||
|
left: `${high.value}px`,
|
||||||
|
right: 'auto',
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
bottom: `${high.value}px`,
|
||||||
|
top: 'auto',
|
||||||
|
transform: 'translate(-50%, 50%)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const precision = computed(() => {
|
||||||
|
// 获取 step 数值精度
|
||||||
|
const strNumArr = props.step.toString().split('.');
|
||||||
|
return strNumArr[1]?.length ?? 0;
|
||||||
|
});
|
||||||
|
const sliderValue = computed(() => {
|
||||||
|
let highValue;
|
||||||
|
if (high.value === sliderSize.value) {
|
||||||
|
highValue = props.max;
|
||||||
|
} else {
|
||||||
|
highValue = fixedDigit(pixelStepOperation(high.value, '/') * props.step + props.min, precision.value);
|
||||||
|
if (props.step > 1) {
|
||||||
|
highValue = Math.round(highValue / props.step) * props.step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.range) {
|
||||||
|
let lowValue = fixedDigit(pixelStepOperation(low.value, '/') * props.step + props.min, precision.value);
|
||||||
|
if (props.step > 1) {
|
||||||
|
lowValue = Math.round(lowValue / props.step) * props.step;
|
||||||
|
}
|
||||||
|
return [lowValue, highValue];
|
||||||
|
}
|
||||||
|
return highValue;
|
||||||
|
});
|
||||||
|
const lowTooltipValue = computed(() => {
|
||||||
|
if (props.range) {
|
||||||
|
return props.formatTooltip((sliderValue.value as number[])[0]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const highTooltipValue = computed(() => {
|
||||||
|
if (props.range) {
|
||||||
|
return props.formatTooltip((sliderValue.value as number[])[1]);
|
||||||
|
}
|
||||||
|
return props.formatTooltip(sliderValue.value as number);
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => [props.min, props.max, props.step, props.vertical, props.value],
|
||||||
|
() => {
|
||||||
|
getSliderPosition();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(sliderValue, (to) => {
|
||||||
|
emits('update:value', to);
|
||||||
|
emits('change', to);
|
||||||
|
});
|
||||||
|
useResizeObserver(sliderRef, () => {
|
||||||
|
getSliderSize();
|
||||||
|
getSliderPosition();
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
getSliderSize();
|
||||||
|
getSliderPosition();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSliderSize() {
|
||||||
|
sliderWidth.value = sliderRef.value.offsetWidth;
|
||||||
|
sliderHeight.value = sliderRef.value.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSliderPosition() {
|
||||||
|
if (props.range) {
|
||||||
|
// 双滑块模式
|
||||||
|
const lowValue = pixelStepOperation((checkLow((props.value as number[])[0]) - props.min) / props.step, '*');
|
||||||
|
low.value = fixedDigit(lowValue, 2);
|
||||||
|
const highValue = pixelStepOperation((checkHigh((props.value as number[])[1]) - props.min) / props.step, '*');
|
||||||
|
high.value = fixedDigit(highValue, 2);
|
||||||
|
} else {
|
||||||
|
const highValue = pixelStepOperation((checkValue(props.value as number) - props.min) / props.step, '*');
|
||||||
|
high.value = fixedDigit(highValue, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkLow(value: number): number {
|
||||||
|
if (value < props.min) {
|
||||||
|
return props.min;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkHigh(value: number): number {
|
||||||
|
if (value > props.max) {
|
||||||
|
return props.max;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValue(value: number): number {
|
||||||
|
if (value < props.min) {
|
||||||
|
return props.min;
|
||||||
|
}
|
||||||
|
if (value > props.max) {
|
||||||
|
return props.max;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixedDigit(num: number, precision: number) {
|
||||||
|
return parseFloat(num.toFixed(precision));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlerBlur(tooltip: HTMLElement) {
|
||||||
|
tooltip.classList.remove('show-handle-tooltip');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlerFocus(handler: HTMLElement, tooltip: HTMLElement) {
|
||||||
|
handler.focus();
|
||||||
|
if (props.tooltip) {
|
||||||
|
tooltip.classList.add('show-handle-tooltip');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickSliderPoint(e: MouseEvent) {
|
||||||
|
let targetDistance; // 目标移动距离
|
||||||
|
if (!props.vertical) {
|
||||||
|
// horizontal
|
||||||
|
const leftX = sliderRef.value.getBoundingClientRect().left; // 滑动条左端距离屏幕可视区域左边界的距离
|
||||||
|
// e.clientX 返回事件被触发时鼠标指针相对于浏览器可视窗口的水平坐标
|
||||||
|
const value = Math.round(pixelStepOperation(e.clientX - leftX, '/'));
|
||||||
|
targetDistance = fixedDigit(pixelStepOperation(value, '*'), 2);
|
||||||
|
} else {
|
||||||
|
const bottomY = sliderRef.value.getBoundingClientRect().bottom; // 滑动条下端距离屏幕可视区域上边界的距离
|
||||||
|
// e.clientY 返回事件被触发时鼠标指针相对于浏览器可视窗口的垂直坐标
|
||||||
|
const value = Math.round(pixelStepOperation(bottomY - e.clientY, '/'));
|
||||||
|
targetDistance = fixedDigit(pixelStepOperation(value, '*'), 2);
|
||||||
|
}
|
||||||
|
if (props.range) {
|
||||||
|
// 双滑块模式
|
||||||
|
if (targetDistance <= low.value) {
|
||||||
|
low.value = targetDistance;
|
||||||
|
handlerFocus(lowHandle.value, lowTooltip.value);
|
||||||
|
} else if (targetDistance >= high.value) {
|
||||||
|
high.value = targetDistance;
|
||||||
|
handlerFocus(highHandle.value, highTooltip.value);
|
||||||
|
} else {
|
||||||
|
if (targetDistance - low.value < high.value - targetDistance) {
|
||||||
|
low.value = targetDistance;
|
||||||
|
handlerFocus(lowHandle.value, lowTooltip.value);
|
||||||
|
} else {
|
||||||
|
high.value = targetDistance;
|
||||||
|
handlerFocus(highHandle.value, highTooltip.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单滑块模式
|
||||||
|
high.value = targetDistance;
|
||||||
|
handlerFocus(highHandle.value, highTooltip.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLowMouseDown() {
|
||||||
|
// 在滑动输入条上拖动较小数值滑块
|
||||||
|
let originalDistance;
|
||||||
|
if (!props.vertical) {
|
||||||
|
// horizontal
|
||||||
|
originalDistance = sliderRef.value.getBoundingClientRect().left; // 滑动条左端距离屏幕可视区域左边界的距离
|
||||||
|
} else {
|
||||||
|
originalDistance = sliderRef.value.getBoundingClientRect().bottom; // 滑动条下端距离屏幕可视区域上边界的距离
|
||||||
|
}
|
||||||
|
window.onmousemove = (e: MouseEvent) => {
|
||||||
|
if (props.tooltip) {
|
||||||
|
lowTooltip.value.classList.add('show-handle-tooltip');
|
||||||
|
}
|
||||||
|
let targetDistance; // 目标移动距离
|
||||||
|
if (!props.vertical) {
|
||||||
|
// horizontal
|
||||||
|
// e.clientX 返回事件被触发时鼠标指针相对于浏览器可视窗口的水平坐标
|
||||||
|
const value = Math.round(pixelStepOperation(e.clientX - originalDistance, '/'));
|
||||||
|
targetDistance = fixedDigit(pixelStepOperation(value, '*'), 2);
|
||||||
|
} else {
|
||||||
|
// vertical
|
||||||
|
// e.clientY 返回事件被触发时鼠标指针相对于浏览器可视窗口的垂直坐标
|
||||||
|
const value = Math.round(pixelStepOperation(originalDistance - e.clientY, '/'));
|
||||||
|
targetDistance = fixedDigit(pixelStepOperation(value, '*'), 2);
|
||||||
|
}
|
||||||
|
if (targetDistance < 0) {
|
||||||
|
low.value = 0;
|
||||||
|
} else if (targetDistance >= 0 && targetDistance <= high.value) {
|
||||||
|
low.value = targetDistance;
|
||||||
|
} else {
|
||||||
|
// targetDistance > high
|
||||||
|
low.value = high.value;
|
||||||
|
highHandle.value.focus();
|
||||||
|
onHighMouseDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.onmouseup = () => {
|
||||||
|
if (props.tooltip) {
|
||||||
|
lowTooltip.value.classList.remove('show-handle-tooltip');
|
||||||
|
}
|
||||||
|
window.onmousemove = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHighMouseDown() {
|
||||||
|
// 在滑动输入条上拖动较大数值滑块
|
||||||
|
let originalDistance;
|
||||||
|
if (!props.vertical) {
|
||||||
|
// horizontal
|
||||||
|
originalDistance = sliderRef.value.getBoundingClientRect().left; // 滑动条左端距离屏幕可视区域左边界的距离
|
||||||
|
} else {
|
||||||
|
originalDistance = sliderRef.value.getBoundingClientRect().bottom; // 滑动条下端距离屏幕可视区域上边界的距离
|
||||||
|
}
|
||||||
|
window.onmousemove = (e: MouseEvent) => {
|
||||||
|
if (props.tooltip) {
|
||||||
|
highTooltip.value.classList.add('show-handle-tooltip');
|
||||||
|
}
|
||||||
|
let targetDistance; // 目标移动距离
|
||||||
|
if (!props.vertical) {
|
||||||
|
// horizontal
|
||||||
|
// e.clientX 返回事件被触发时鼠标指针相对于浏览器可视窗口的水平坐标
|
||||||
|
const value = Math.round(pixelStepOperation(e.clientX - originalDistance, '/'));
|
||||||
|
targetDistance = fixedDigit(pixelStepOperation(value, '*'), 2);
|
||||||
|
} else {
|
||||||
|
// vertical
|
||||||
|
// e.clientY 返回事件被触发时鼠标指针相对于浏览器可视窗口的垂直坐标
|
||||||
|
const value = Math.round(pixelStepOperation(originalDistance - e.clientY, '/'));
|
||||||
|
targetDistance = fixedDigit(pixelStepOperation(value, '*'), 2);
|
||||||
|
}
|
||||||
|
if (targetDistance > sliderSize.value) {
|
||||||
|
high.value = sliderSize.value;
|
||||||
|
} else if (low.value <= targetDistance && targetDistance <= sliderSize.value) {
|
||||||
|
high.value = targetDistance;
|
||||||
|
} else {
|
||||||
|
// targetDistance < low
|
||||||
|
high.value = low.value;
|
||||||
|
if (props.range) {
|
||||||
|
lowHandle.value.focus();
|
||||||
|
onLowMouseDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.onmouseup = () => {
|
||||||
|
if (props.tooltip) {
|
||||||
|
highTooltip.value.classList.remove('show-handle-tooltip');
|
||||||
|
}
|
||||||
|
window.onmousemove = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLowSlide(source: number, place: string) {
|
||||||
|
const targetDistance = pixelStepOperation(source, '-');
|
||||||
|
if (place === 'low') {
|
||||||
|
// 左/下滑块左/下移
|
||||||
|
if (targetDistance < 0) {
|
||||||
|
low.value = 0;
|
||||||
|
} else {
|
||||||
|
low.value = targetDistance;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 右/上滑块左/下移
|
||||||
|
if (targetDistance >= low.value) {
|
||||||
|
high.value = targetDistance;
|
||||||
|
} else {
|
||||||
|
high.value = low.value;
|
||||||
|
low.value = targetDistance;
|
||||||
|
lowHandle.value.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHighSlide(source: number, place: string) {
|
||||||
|
const targetDistance = pixelStepOperation(source, '+');
|
||||||
|
if (place === 'high') {
|
||||||
|
// 右/上滑块右/上移
|
||||||
|
if (targetDistance > sliderSize.value) {
|
||||||
|
high.value = sliderSize.value;
|
||||||
|
} else {
|
||||||
|
high.value = targetDistance;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 左/下滑块右/上移
|
||||||
|
if (targetDistance <= high.value) {
|
||||||
|
low.value = targetDistance;
|
||||||
|
} else {
|
||||||
|
low.value = high.value;
|
||||||
|
high.value = targetDistance;
|
||||||
|
highHandle.value.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pixelStepOperation(target: number, operator: '+' | '-' | '*' | '/'): number {
|
||||||
|
if (operator === '+') {
|
||||||
|
return target + (sliderSize.value * props.step) / (props.max - props.min);
|
||||||
|
}
|
||||||
|
if (operator === '-') {
|
||||||
|
return target - (sliderSize.value * props.step) / (props.max - props.min);
|
||||||
|
}
|
||||||
|
if (operator === '*') {
|
||||||
|
return (target * sliderSize.value * props.step) / (props.max - props.min);
|
||||||
|
}
|
||||||
|
if (operator === '/') {
|
||||||
|
return (target * (props.max - props.min)) / (sliderSize.value * props.step);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="sliderRef"
|
||||||
|
class="m-slider"
|
||||||
|
:class="{
|
||||||
|
'slider-horizontal': !vertical,
|
||||||
|
'slider-vertical': vertical,
|
||||||
|
'slider-disabled': disabled
|
||||||
|
}"
|
||||||
|
:style="sliderStyle"
|
||||||
|
@click="disabled ? () => false : onClickSliderPoint($event)"
|
||||||
|
>
|
||||||
|
<div class="slider-rail"></div>
|
||||||
|
<div class="slider-track" :style="trackStyle"></div>
|
||||||
|
<div
|
||||||
|
v-if="range"
|
||||||
|
tabindex="0"
|
||||||
|
ref="lowHandle"
|
||||||
|
class="slider-handle"
|
||||||
|
:style="lowHandleStyle"
|
||||||
|
@keydown.left.prevent="disabled ? () => false : onLowSlide(low, 'low')"
|
||||||
|
@keydown.right.prevent="disabled ? () => false : onHighSlide(low, 'low')"
|
||||||
|
@keydown.down.prevent="disabled ? () => false : onLowSlide(low, 'low')"
|
||||||
|
@keydown.up.prevent="disabled ? () => false : onHighSlide(low, 'low')"
|
||||||
|
@mousedown="disabled ? () => false : onLowMouseDown()"
|
||||||
|
@blur="tooltip && !disabled ? handlerBlur(lowTooltip) : () => false"
|
||||||
|
>
|
||||||
|
<div v-if="tooltip" ref="lowTooltip" class="handle-tooltip">
|
||||||
|
{{ lowTooltipValue }}
|
||||||
|
<div class="tooltip-arrow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
ref="highHandle"
|
||||||
|
class="slider-handle"
|
||||||
|
:style="highHandleStyle"
|
||||||
|
@keydown.left.prevent="disabled ? () => false : onLowSlide(high, 'high')"
|
||||||
|
@keydown.right.prevent="disabled ? () => false : onHighSlide(high, 'high')"
|
||||||
|
@keydown.down.prevent="disabled ? () => false : onLowSlide(high, 'high')"
|
||||||
|
@keydown.up.prevent="disabled ? () => false : onHighSlide(high, 'high')"
|
||||||
|
@mousedown="disabled ? () => false : onHighMouseDown()"
|
||||||
|
@blur="tooltip && !disabled ? handlerBlur(highTooltip) : () => false"
|
||||||
|
>
|
||||||
|
<div v-if="tooltip" ref="highTooltip" class="handle-tooltip">
|
||||||
|
{{ highTooltipValue }}
|
||||||
|
<div class="tooltip-arrow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-slider {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: none; // 禁用元素上的所有手势,使用自己的拖动和缩放api
|
||||||
|
.slider-rail {
|
||||||
|
// 灰色未选择滑动条背景色
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-track {
|
||||||
|
// 蓝色已选择滑动条背景色
|
||||||
|
position: absolute;
|
||||||
|
background-color: #91caff;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.slider-rail {
|
||||||
|
// 灰色未选择滑动条背景色
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-track {
|
||||||
|
// 蓝色已选择滑动条背景色
|
||||||
|
background: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-handle {
|
||||||
|
// 滑块
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 2px #91caff;
|
||||||
|
border-radius: 50%;
|
||||||
|
outline: none;
|
||||||
|
transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
height 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -2px;
|
||||||
|
top: -2px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-focus-handle {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
box-shadow: 0 0 0 4px @themeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
.hover-focus-handle();
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: -5px;
|
||||||
|
top: -5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 32px;
|
||||||
|
max-width: 250px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
clip-path: path('M 0 8 A 4 4 0 0 0 2.82842712474619 6.82842712474619 L 6.585786437626905 3.0710678118654755 A 2 2 0 0 1 9.414213562373096 3.0710678118654755 L 13.17157287525381 6.82842712474619 A 4 4 0 0 0 16 8 Z');
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 8.970562748477143px;
|
||||||
|
height: 8.970562748477143px;
|
||||||
|
bottom: 0;
|
||||||
|
inset-inline: 0;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 0 0 2px 0;
|
||||||
|
transform: translateY(50%) rotate(-135deg);
|
||||||
|
box-shadow: 3px 3px 7px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 0;
|
||||||
|
background: transparent;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-horizontal {
|
||||||
|
padding-block: 4px;
|
||||||
|
height: 12px;
|
||||||
|
margin: 10px 5px;
|
||||||
|
|
||||||
|
.slider-rail {
|
||||||
|
height: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-track {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-handle {
|
||||||
|
top: 50%;
|
||||||
|
|
||||||
|
.handle-tooltip {
|
||||||
|
top: -32px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) scale(0.8);
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateX(-50%) translateY(100%) rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-handle-tooltip {
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.handle-tooltip {
|
||||||
|
.show-handle-tooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-vertical {
|
||||||
|
padding-inline: 4px;
|
||||||
|
width: 12px;
|
||||||
|
margin: 5px 10px;
|
||||||
|
|
||||||
|
.slider-rail {
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-track {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-handle {
|
||||||
|
left: 50%;
|
||||||
|
|
||||||
|
.handle-tooltip {
|
||||||
|
left: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(16px, -50%) scale(0.8);
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-50%) translateX(-100%) rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-handle-tooltip {
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translate(16px, -50%) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.handle-tooltip {
|
||||||
|
.show-handle-tooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.slider-rail {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-track {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-handle {
|
||||||
|
box-shadow: 0 0 0 2px #bfbfbf;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
box-shadow: 0 0 0 2px #bfbfbf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.slider-rail {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-track {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
src/components/MyUI/Space/Space.vue
Normal file
79
src/components/MyUI/Space/Space.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
interface Props {
|
||||||
|
width?: string | number // 区域总宽度,单位 px
|
||||||
|
align?: 'stretch' | 'start' | 'end' | 'center' | 'baseline' // 垂直排列方式
|
||||||
|
vertical?: boolean // 是否为垂直布局
|
||||||
|
gap?: number | number[] | 'small' | 'middle' | 'large' // 间距大小,数组时表示: [水平间距, 垂直间距]
|
||||||
|
wrap?: boolean // 是否自动换行,仅在 horizontal 时有效
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
width: 'auto',
|
||||||
|
align: 'start',
|
||||||
|
vertical: false,
|
||||||
|
gap: 'middle',
|
||||||
|
wrap: true
|
||||||
|
});
|
||||||
|
const spaceWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const gapValue = computed(() => {
|
||||||
|
if (typeof props.gap === 'number') {
|
||||||
|
return `${props.gap}px`;
|
||||||
|
}
|
||||||
|
if (Array.isArray(props.gap)) {
|
||||||
|
return `${props.gap[1]}px ${props.gap[0]}px`;
|
||||||
|
}
|
||||||
|
if (['small', 'middle', 'large'].includes(props.gap)) {
|
||||||
|
const gapMap = {
|
||||||
|
small: '8px',
|
||||||
|
middle: '16px',
|
||||||
|
large: '24px'
|
||||||
|
};
|
||||||
|
return gapMap[props.gap];
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-space"
|
||||||
|
:class="[`space-${align}`, { 'space-vertical': vertical, 'space-wrap': wrap }]"
|
||||||
|
:style="`width: ${spaceWidth}; gap: ${gapValue}; margin-bottom: -${Array.isArray(props.gap) && wrap ? props.gap[1] : 0}px;`"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-space {
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
transition: all 0.3s;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.space-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.space-stretch {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.space-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.space-end {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.space-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.space-baseline {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.space-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
99
src/components/MyUI/Statistic/Statistic.vue
Normal file
99
src/components/MyUI/Statistic/Statistic.vue
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {formatNumber, useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string // 数值的标题 string | slot
|
||||||
|
value?: string | number // 数值的内容 string | number | slot
|
||||||
|
valueStyle?: CSSProperties // 设置数值的样式
|
||||||
|
precision?: number // 数值精度
|
||||||
|
prefix?: string // 设置数值的前缀 string | slot
|
||||||
|
suffix?: string // 设置数值的后缀 string | slot
|
||||||
|
separator?: string // 设置千分位标识符
|
||||||
|
formatter?: (value: string) => string // 自定义数值展示
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: undefined,
|
||||||
|
value: undefined,
|
||||||
|
valueStyle: () => ({}),
|
||||||
|
precision: 0,
|
||||||
|
prefix: undefined,
|
||||||
|
suffix: undefined,
|
||||||
|
separator: ',',
|
||||||
|
formatter: (value: string) => value
|
||||||
|
});
|
||||||
|
const slotsExist = useSlotsExist(['title', 'prefix', 'suffix']);
|
||||||
|
const showValue = computed(() => {
|
||||||
|
return props.formatter(formatNumber(props.value || '', props.precision, props.separator));
|
||||||
|
});
|
||||||
|
const showTitle = computed(() => {
|
||||||
|
return slotsExist.title || props.title;
|
||||||
|
});
|
||||||
|
const showPrefix = computed(() => {
|
||||||
|
return slotsExist.prefix || props.prefix;
|
||||||
|
});
|
||||||
|
const showSuffix = computed(() => {
|
||||||
|
return slotsExist.suffix || props.suffix;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-statistic">
|
||||||
|
<div v-if="showTitle" class="statistic-title">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</div>
|
||||||
|
<div class="statistic-content" :style="valueStyle">
|
||||||
|
<span v-if="showPrefix" class="statistic-prefix">
|
||||||
|
<slot name="prefix">{{ prefix }}</slot>
|
||||||
|
</span>
|
||||||
|
<span class="statistic-value">
|
||||||
|
<slot>{{ showValue }}</slot>
|
||||||
|
</span>
|
||||||
|
<span v-if="showSuffix" class="statistic-suffix">
|
||||||
|
<slot name="suffix">{{ suffix }}</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-statistic {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
|
||||||
|
.statistic-title {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistic-content {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
.statistic-prefix {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistic-value {
|
||||||
|
display: inline-block;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistic-suffix {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
617
src/components/MyUI/Steps/Steps.vue
Normal file
617
src/components/MyUI/Steps/Steps.vue
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed} from 'vue';
|
||||||
|
|
||||||
|
interface Step {
|
||||||
|
title?: string // 标题
|
||||||
|
description?: string // 描述
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
steps?: Step[] // 步骤数组
|
||||||
|
width?: number | string // 步骤条总宽度,单位 px
|
||||||
|
size?: 'default' | 'small' // 步骤条大小
|
||||||
|
vertical?: boolean // 是否使用垂直步骤条,当 vertical: true 时,labelPlacement 自动设为 right
|
||||||
|
labelPlacement?: 'right' | 'bottom' // 标签放置位置,默认放图标右侧,可选 bottom 放图标下方
|
||||||
|
dotted?: boolean // 是否使用点状步骤条,当 dotted: true 且 vertical: false 时,labelPlacement 将自动设为 bottom
|
||||||
|
current?: number // (v-model) 当前选中的步骤,设置 v-model 后,Steps 变为可点击状态。从 1 开始计数
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
steps: () => [],
|
||||||
|
width: 'auto',
|
||||||
|
size: 'default',
|
||||||
|
vertical: false,
|
||||||
|
labelPlacement: 'right',
|
||||||
|
dotted: false,
|
||||||
|
current: 1
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['update:current', 'change']);
|
||||||
|
const totalWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const totalSteps = computed(() => {
|
||||||
|
// 步骤总数
|
||||||
|
return props.steps.length;
|
||||||
|
});
|
||||||
|
const currentStep = computed(() => {
|
||||||
|
if (props.current < 1) {
|
||||||
|
return 1;
|
||||||
|
} else if (props.current > totalSteps.value + 1) {
|
||||||
|
return totalSteps.value + 1;
|
||||||
|
} else {
|
||||||
|
return props.current;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChange(index: number) {
|
||||||
|
// 点击切换选择步骤
|
||||||
|
if (currentStep.value !== index) {
|
||||||
|
emits('update:current', index);
|
||||||
|
emits('change', index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-steps"
|
||||||
|
:class="{
|
||||||
|
'steps-small': size === 'small',
|
||||||
|
'steps-vertical': vertical,
|
||||||
|
'steps-label-bottom': !vertical && (labelPlacement === 'bottom' || dotted),
|
||||||
|
'steps-dotted': dotted
|
||||||
|
}"
|
||||||
|
:style="`width: ${totalWidth};`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="m-steps-item"
|
||||||
|
:class="{
|
||||||
|
'steps-finish': currentStep > index + 1,
|
||||||
|
'steps-process': currentStep === index + 1,
|
||||||
|
'steps-wait': currentStep < index + 1
|
||||||
|
}"
|
||||||
|
v-for="(step, index) in steps"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<div tabindex="0" class="steps-info-wrap" @click="onChange(index + 1)">
|
||||||
|
<div class="steps-tail"></div>
|
||||||
|
<div class="steps-icon">
|
||||||
|
<template v-if="!dotted">
|
||||||
|
<span v-if="currentStep <= index + 1" class="steps-num">{{ index + 1 }}</span>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="check"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="steps-dot"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="m-steps-content">
|
||||||
|
<div class="steps-title">{{ step.title }}</div>
|
||||||
|
<div v-if="step.description" class="steps-description">{{ step.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-steps {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:not(.steps-label-bottom) {
|
||||||
|
.m-steps-item .steps-info-wrap {
|
||||||
|
.steps-tail {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-item {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1; // 弹性盒模型对象的子元素都有相同的长度,且忽略它们内部的内容
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
flex: none;
|
||||||
|
|
||||||
|
.steps-info-wrap {
|
||||||
|
.m-steps-content .steps-title {
|
||||||
|
padding-right: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-tail {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-info-wrap {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.steps-tail {
|
||||||
|
height: 9px;
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: rgba(5, 5, 5, 0.06);
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
.steps-num {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
color: @themeColor;
|
||||||
|
fill: currentColor;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-dot {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
.steps-title {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 32px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
padding-right: 16px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: #e8e8e8;
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 100%;
|
||||||
|
display: block;
|
||||||
|
width: 3000px;
|
||||||
|
height: 1px;
|
||||||
|
content: '';
|
||||||
|
cursor: auto;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-description {
|
||||||
|
max-width: 140px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
line-height: 22px;
|
||||||
|
word-break: break-all;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-finish {
|
||||||
|
.steps-info-wrap {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.steps-tail {
|
||||||
|
&::after {
|
||||||
|
background-color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-icon {
|
||||||
|
background-color: #e6f4ff;
|
||||||
|
border-color: #e6f4ff;
|
||||||
|
|
||||||
|
.steps-dot {
|
||||||
|
background: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
.steps-title {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-description {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.steps-icon {
|
||||||
|
border-color: @themeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
.steps-title,
|
||||||
|
.steps-description {
|
||||||
|
color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-process {
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-icon {
|
||||||
|
background-color: @themeColor;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||||
|
border-color: @themeColor;
|
||||||
|
|
||||||
|
.steps-num {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-dot {
|
||||||
|
background: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
.steps-title,
|
||||||
|
.steps-description {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-wait {
|
||||||
|
.steps-info-wrap {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.steps-icon {
|
||||||
|
border-color: @themeColor;
|
||||||
|
|
||||||
|
.steps-num {
|
||||||
|
color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
.steps-title,
|
||||||
|
.steps-description {
|
||||||
|
color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-icon {
|
||||||
|
.steps-dot {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-small {
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.m-steps-item {
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
.steps-num,
|
||||||
|
.icon-svg {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
.steps-title {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
padding-right: 12px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-label-bottom {
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
.m-steps-item {
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-tail {
|
||||||
|
margin-left: 56px;
|
||||||
|
padding: 4px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-icon {
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
display: block;
|
||||||
|
width: 112px;
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.steps-title {
|
||||||
|
padding-right: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-dotted {
|
||||||
|
.m-steps-item {
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-tail {
|
||||||
|
height: 3px;
|
||||||
|
top: 2.5px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-inline: 70px 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
height: 3px;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-icon {
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-left: 66px;
|
||||||
|
padding-right: 0;
|
||||||
|
line-height: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
width: 140px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.steps-title {
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-process {
|
||||||
|
.steps-info-wrap .steps-icon {
|
||||||
|
top: -1px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
line-height: 10px;
|
||||||
|
margin-left: 65px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-vertical {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
.m-steps-item {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-tail {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
.steps-title {
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-tail {
|
||||||
|
top: 0;
|
||||||
|
left: 15px;
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 38px 0 6px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-icon {
|
||||||
|
float: left;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
display: block;
|
||||||
|
min-height: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.steps-title {
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-description {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-small.steps-vertical {
|
||||||
|
.m-steps-item {
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-tail {
|
||||||
|
left: 11px;
|
||||||
|
padding: 30px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
.steps-title {
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-vertical.steps-dotted {
|
||||||
|
.m-steps-item {
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-tail {
|
||||||
|
top: 12px;
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px 0 8px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
margin-left: 3.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-icon {
|
||||||
|
position: static;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-left: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-steps-content {
|
||||||
|
width: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-process {
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-icon {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 11px;
|
||||||
|
top: 0;
|
||||||
|
left: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-small.steps-vertical.steps-dotted {
|
||||||
|
.m-steps-item {
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-tail {
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-icon {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-process {
|
||||||
|
.steps-info-wrap {
|
||||||
|
.steps-icon {
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
223
src/components/MyUI/Swiper/Swiper.vue
Normal file
223
src/components/MyUI/Swiper/Swiper.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
import type {Swiper as SwiperTypes} from 'swiper/types';
|
||||||
|
import {Swiper, SwiperSlide} from 'swiper/vue';
|
||||||
|
import {
|
||||||
|
Autoplay,
|
||||||
|
EffectCards,
|
||||||
|
EffectCoverflow,
|
||||||
|
EffectCreative,
|
||||||
|
EffectCube,
|
||||||
|
EffectFade,
|
||||||
|
EffectFlip,
|
||||||
|
Mousewheel,
|
||||||
|
Navigation,
|
||||||
|
Pagination
|
||||||
|
} from 'swiper/modules';
|
||||||
|
import 'swiper/less';
|
||||||
|
import 'swiper/less/navigation';
|
||||||
|
import 'swiper/less/pagination';
|
||||||
|
import 'swiper/less/effect-fade';
|
||||||
|
import 'swiper/less/effect-cube';
|
||||||
|
import 'swiper/less/effect-flip';
|
||||||
|
import 'swiper/less/effect-coverflow';
|
||||||
|
import 'swiper/less/effect-cards';
|
||||||
|
import 'swiper/less/effect-creative';
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
name?: string // 图片名称
|
||||||
|
src: string // 图片地址
|
||||||
|
link?: string // 图片跳转链接
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
images?: Image[] // 轮播图片数组
|
||||||
|
width?: number | string // 轮播区域宽度,单位 px
|
||||||
|
height?: number | string // 轮播区域高度,单位 px
|
||||||
|
mode?: 'banner' | 'carousel' | 'broadcast' // banner: 轮播图模式; carousel: 走马灯模式; broadcast: 信息展播模式
|
||||||
|
navigation?: boolean // 是否显示导航
|
||||||
|
effect?: 'slide' | 'fade' | 'cube' | 'flip' | 'coverflow' | 'cards' | 'creative' // 切换动画效果
|
||||||
|
delay?: number // 自动切换的时间间隔,仅当 mode: 'banner' 时生效,单位 ms
|
||||||
|
speed?: number // 切换过渡的动画持续时间,单位 ms
|
||||||
|
loop?: boolean // 是否循环切换
|
||||||
|
pauseOnMouseEnter?: boolean // 当鼠标移入走马灯时,是否暂停自动轮播,仅当 mode: 'banner' 或 mode: 'carousel' 时生效
|
||||||
|
swipe?: boolean // 是否可以鼠标拖动
|
||||||
|
preloaderColor?: 'theme' | 'white' | 'black' // 预加载时的 loading 颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
images: () => [],
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
mode: 'banner',
|
||||||
|
navigation: false,
|
||||||
|
effect: 'slide',
|
||||||
|
delay: 3000,
|
||||||
|
speed: 300,
|
||||||
|
loop: true,
|
||||||
|
pauseOnMouseEnter: false,
|
||||||
|
swipe: true,
|
||||||
|
preloaderColor: 'theme'
|
||||||
|
});
|
||||||
|
const autoplayBanner = ref({
|
||||||
|
delay: props.delay,
|
||||||
|
disableOnInteraction: false, // 用户操作 swiper 之后,是否禁止 autoplay。默认为 true:停止。
|
||||||
|
pauseOnMouseEnter: props.pauseOnMouseEnter // 鼠标置于 swiper 时暂停自动切换,鼠标离开时恢复自动切换,默认 false
|
||||||
|
});
|
||||||
|
const modulesCarousel = ref([Autoplay]);
|
||||||
|
const autoplayCarousel = ref<object | boolean>({
|
||||||
|
delay: 0,
|
||||||
|
disableOnInteraction: false
|
||||||
|
});
|
||||||
|
const modulesBroadcast = ref([Navigation, Pagination, Mousewheel]);
|
||||||
|
const emits = defineEmits(['swiper', 'change']);
|
||||||
|
const swiperWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const swiperHeight = computed(() => {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
} else {
|
||||||
|
return props.height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const modulesBanner = computed(() => {
|
||||||
|
const modules = [Navigation, Pagination, Autoplay];
|
||||||
|
const effectMoudles = {
|
||||||
|
fade: EffectFade,
|
||||||
|
cube: EffectCube,
|
||||||
|
flip: EffectFlip,
|
||||||
|
coverflow: EffectCoverflow,
|
||||||
|
cards: EffectCards,
|
||||||
|
creative: EffectCreative
|
||||||
|
};
|
||||||
|
if (props.effect !== 'slide') {
|
||||||
|
modules.push(effectMoudles[props.effect]);
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSwiper(swiper: SwiperTypes) {
|
||||||
|
emits('swiper', swiper);
|
||||||
|
if (props.mode === 'carousel' && props.pauseOnMouseEnter) {
|
||||||
|
swiper.el.onmouseenter = () => {
|
||||||
|
// 移入暂停
|
||||||
|
swiper.autoplay.stop();
|
||||||
|
};
|
||||||
|
swiper.el.onmouseleave = () => {
|
||||||
|
// 移出启动
|
||||||
|
swiper.autoplay.start();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageName(image: Image) {
|
||||||
|
// 从图片地址 src 中获取图片名称
|
||||||
|
if (image.name) {
|
||||||
|
return image.name;
|
||||||
|
} else {
|
||||||
|
const res = image.src.split('?')[0].split('/');
|
||||||
|
return res[res.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Swiper
|
||||||
|
v-if="mode === 'banner'"
|
||||||
|
:class="{ 'swiper-no-swiping': !swipe }"
|
||||||
|
:style="`width: ${swiperWidth}; height: ${swiperHeight};`"
|
||||||
|
:modules="modulesBanner"
|
||||||
|
:navigation="navigation"
|
||||||
|
:slides-per-view="1"
|
||||||
|
:autoplay="autoplayBanner"
|
||||||
|
:effect="effect"
|
||||||
|
:speed="speed"
|
||||||
|
:loop="loop"
|
||||||
|
lazy
|
||||||
|
@swiper="onSwiper"
|
||||||
|
@slideChange="(swiper) => $emit('change', swiper)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<SwiperSlide v-for="(image, index) in images" :key="index">
|
||||||
|
<component :is="image.link ? 'a' : 'div'" class="swiper-link" :href="image.link" target="_blank">
|
||||||
|
<img class="swiper-image" :src="image.src" :alt="getImageName(image)" loading="lazy"/>
|
||||||
|
</component>
|
||||||
|
<div :class="`swiper-lazy-preloader swiper-lazy-preloader-${preloaderColor}`"></div>
|
||||||
|
</SwiperSlide>
|
||||||
|
</Swiper>
|
||||||
|
<Swiper
|
||||||
|
v-if="mode === 'carousel'"
|
||||||
|
class="swiper-no-swiping"
|
||||||
|
:style="`width: ${swiperWidth}; height: ${swiperHeight};`"
|
||||||
|
:modules="modulesCarousel"
|
||||||
|
:autoplay="autoplayCarousel"
|
||||||
|
:speed="speed"
|
||||||
|
:loop="loop"
|
||||||
|
lazy
|
||||||
|
@swiper="onSwiper"
|
||||||
|
@slideChange="(swiper) => $emit('change', swiper)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<SwiperSlide v-for="(image, index) in images" :key="index">
|
||||||
|
<component :is="image.link ? 'a' : 'div'" class="swiper-link" :href="image.link" target="_blank">
|
||||||
|
<img class="swiper-image" :src="image.src" :alt="getImageName(image)" loading="lazy"/>
|
||||||
|
</component>
|
||||||
|
<div :class="`swiper-lazy-preloader swiper-lazy-preloader-${preloaderColor}`"></div>
|
||||||
|
</SwiperSlide>
|
||||||
|
</Swiper>
|
||||||
|
<Swiper
|
||||||
|
v-if="mode === 'broadcast'"
|
||||||
|
:style="`width: ${swiperWidth}; height: ${swiperHeight};`"
|
||||||
|
:modules="modulesBroadcast"
|
||||||
|
:navigation="navigation"
|
||||||
|
:speed="speed"
|
||||||
|
:loop="loop"
|
||||||
|
lazy
|
||||||
|
@swiper="onSwiper"
|
||||||
|
@slideChange="(swiper) => $emit('change', swiper)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<SwiperSlide v-for="(image, index) in images" :key="index">
|
||||||
|
<component :is="image.link ? 'a' : 'div'" class="swiper-link" :href="image.link" target="_blank">
|
||||||
|
<img class="swiper-image" :src="image.src" :alt="getImageName(image)" loading="lazy"/>
|
||||||
|
</component>
|
||||||
|
<div :class="`swiper-lazy-preloader swiper-lazy-preloader-${preloaderColor}`"></div>
|
||||||
|
</SwiperSlide>
|
||||||
|
</Swiper>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.swiper-link {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.swiper-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper {
|
||||||
|
--swiper-theme-color: @themeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.swiper-wrapper) {
|
||||||
|
// 自动切换过渡效果设置
|
||||||
|
transition-timing-function: linear; // 线性过渡模拟走马灯效果
|
||||||
|
-webkit-transition-timing-function: linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.swiper-pagination-bullet) {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-lazy-preloader-theme {
|
||||||
|
--swiper-preloader-color: @themeColor;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
380
src/components/MyUI/Switch/Switch.vue
Normal file
380
src/components/MyUI/Switch/Switch.vue
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {nextTick, ref} from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
checked?: string // 选中时的内容 string | slot
|
||||||
|
checkedValue?: boolean | string | number // 选中时的值
|
||||||
|
unchecked?: string // 未选中时的内容 string | slot
|
||||||
|
uncheckedValue?: boolean | string | number // 未选中时的值
|
||||||
|
loading?: boolean // 是否加载中
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
size?: 'small' | 'middle' | 'large' // 开关大小
|
||||||
|
rippleColor?: string // 点击时的波纹颜色,当自定义选中颜色时需要设置
|
||||||
|
circleStyle?: CSSProperties // 圆点样式
|
||||||
|
modelValue?: boolean | string | number // (v-model) 指定当前是否选中
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
checked: undefined,
|
||||||
|
checkedValue: true,
|
||||||
|
unchecked: undefined,
|
||||||
|
uncheckedValue: false,
|
||||||
|
loading: false,
|
||||||
|
disabled: false,
|
||||||
|
size: 'middle',
|
||||||
|
rippleColor: '#1677ff',
|
||||||
|
circleStyle: () => ({}),
|
||||||
|
modelValue: false
|
||||||
|
});
|
||||||
|
const wave = ref(false);
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change']);
|
||||||
|
|
||||||
|
function onSwitch() {
|
||||||
|
if (props.modelValue === props.checkedValue) {
|
||||||
|
emit('update:modelValue', props.uncheckedValue);
|
||||||
|
emit('change', props.uncheckedValue);
|
||||||
|
} else {
|
||||||
|
emit('update:modelValue', props.checkedValue);
|
||||||
|
emit('change', props.checkedValue);
|
||||||
|
}
|
||||||
|
if (wave.value) {
|
||||||
|
wave.value = false;
|
||||||
|
nextTick(() => {
|
||||||
|
wave.value = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wave.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWaveEnd() {
|
||||||
|
wave.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="m-switch"
|
||||||
|
:class="{
|
||||||
|
'switch-loading': loading,
|
||||||
|
'switch-small': size === 'small',
|
||||||
|
'switch-large': size === 'large',
|
||||||
|
'switch-checked': modelValue === checkedValue,
|
||||||
|
'switch-disabled': disabled
|
||||||
|
}"
|
||||||
|
:style="`--ripple-color: ${rippleColor};`"
|
||||||
|
@click="disabled || loading ? () => false : onSwitch()"
|
||||||
|
>
|
||||||
|
<div class="switch-inner">
|
||||||
|
<span class="inner-checked">
|
||||||
|
<slot name="checked">{{ checked }}</slot>
|
||||||
|
</span>
|
||||||
|
<span class="inner-unchecked">
|
||||||
|
<slot name="unchecked">{{ unchecked }}</slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="switch-circle" :style="circleStyle">
|
||||||
|
<svg v-if="loading" class="circular" viewBox="0 0 50 50">
|
||||||
|
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
|
||||||
|
</svg>
|
||||||
|
<slot name="node" :checked="modelValue"></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="!disabled" class="switch-wave" :class="{ 'wave-active': wave }" @animationend="onWaveEnd"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 22px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
border-radius: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover:not(.switch-disabled) {
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-inner {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 100px;
|
||||||
|
height: 100%;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 9px;
|
||||||
|
transition: padding-left 0.2s ease-in-out,
|
||||||
|
padding-right 0.2s ease-in-out;
|
||||||
|
|
||||||
|
.inner-checked {
|
||||||
|
margin-left: calc(-100% + 22px - 48px);
|
||||||
|
margin-right: calc(100% - 22px + 48px);
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: margin-left 0.2s ease-in-out,
|
||||||
|
margin-right 0.2s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-unchecked {
|
||||||
|
margin-top: -22px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: margin-left 0.2s ease-in-out,
|
||||||
|
margin-right 0.2s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-circle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 100%;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
||||||
|
.circular {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
animation: loading-rotate 2s linear infinite;
|
||||||
|
-webkit-animation: loading-rotate 2s linear infinite;
|
||||||
|
@keyframes loading-rotate {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke: @themeColor;
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
animation: loading-dash 1.5s ease-in-out infinite;
|
||||||
|
-webkit-animation: loading-dash 1.5s ease-in-out infinite;
|
||||||
|
@keyframes loading-dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -40px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-loading {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.65;
|
||||||
|
|
||||||
|
.switch-inner,
|
||||||
|
.switch-circle {
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-small {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
.switch-inner {
|
||||||
|
padding-left: 18px;
|
||||||
|
padding-right: 6px;
|
||||||
|
|
||||||
|
.inner-checked {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: calc(-100% + 16px - 36px);
|
||||||
|
margin-right: calc(100% - 16px + 36px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-unchecked {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: -16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-circle {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
|
||||||
|
.circular {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
|
||||||
|
.path {
|
||||||
|
stroke-width: 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-large {
|
||||||
|
min-width: 60px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
|
||||||
|
.switch-inner {
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 12px;
|
||||||
|
|
||||||
|
.inner-checked {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: calc(-100% + 28px - 60px);
|
||||||
|
margin-right: calc(100% - 28px + 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-unchecked {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-top: -28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-circle {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
.circular {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
.path {
|
||||||
|
stroke-width: 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-checked {
|
||||||
|
background: @themeColor;
|
||||||
|
|
||||||
|
&:hover:not(.switch-disabled) {
|
||||||
|
background: #4096ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-inner {
|
||||||
|
padding-left: 9px;
|
||||||
|
padding-right: 24px;
|
||||||
|
|
||||||
|
.inner-checked {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-unchecked {
|
||||||
|
margin-left: calc(100% - 22px + 48px);
|
||||||
|
margin-right: calc(-100% + 22px - 48px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-circle {
|
||||||
|
left: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-small.switch-checked {
|
||||||
|
.switch-inner {
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right: 18px;
|
||||||
|
|
||||||
|
.inner-unchecked {
|
||||||
|
margin-left: calc(100% - 16px + 36px);
|
||||||
|
margin-right: calc(-100% + 16px - 36px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-circle {
|
||||||
|
left: calc(100% - 14px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-large.switch-checked {
|
||||||
|
.switch-inner {
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 30px;
|
||||||
|
|
||||||
|
.inner-unchecked {
|
||||||
|
margin-left: calc(100% - 28px + 60px);
|
||||||
|
margin-right: calc(-100% + 28px - 60px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-circle {
|
||||||
|
left: calc(100% - 26px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.65;
|
||||||
|
|
||||||
|
.switch-circle {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-wave {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
animation-duration: 0.6s;
|
||||||
|
animation-timing-function: cubic-bezier(0, 0, 0.2, 1), cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-active {
|
||||||
|
z-index: 1;
|
||||||
|
animation-name: wave-spread, wave-opacity;
|
||||||
|
@keyframes wave-spread {
|
||||||
|
from {
|
||||||
|
box-shadow: 0 0 0.5px 0 var(--ripple-color);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
box-shadow: 0 0 0.5px 5px var(--ripple-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes wave-opacity {
|
||||||
|
from {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
172
src/components/MyUI/Table/Table.vue
Normal file
172
src/components/MyUI/Table/Table.vue
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Spin from '../Spin/Spin.vue';
|
||||||
|
import Empty from '../Empty/Empty.vue';
|
||||||
|
import Pagination from '../Pagination/Pagination.vue';
|
||||||
|
|
||||||
|
interface Column {
|
||||||
|
title?: string // 列头显示文字
|
||||||
|
width?: number | string // 列宽度,单位 px
|
||||||
|
dataIndex: string // 列数据字符索引
|
||||||
|
slot?: string // 列插槽名称索引
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
columns?: Column[] // 表格列的配置项
|
||||||
|
dataSource?: any[] // 表格数据数组
|
||||||
|
loading?: boolean // 是否加载中
|
||||||
|
spinProps?: object // Spin 组件属性配置,参考 Spin Props,用于配置数据加载中样式
|
||||||
|
emptyProps?: object // Empty 组件属性配置,参考 Empty Props,用于配置暂无数据样式
|
||||||
|
showPagination?: boolean // 是否显示分页
|
||||||
|
pagination?: object // Pagination 组件属性配置,参考 Pagination Props,用于配置分页功能
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
columns: () => [],
|
||||||
|
dataSource: () => [],
|
||||||
|
loading: false,
|
||||||
|
spinProps: () => ({}),
|
||||||
|
emptyProps: () => ({}),
|
||||||
|
showPagination: true,
|
||||||
|
pagination: () => ({})
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['change']);
|
||||||
|
|
||||||
|
function onChange(page: number, pageSize: number) {
|
||||||
|
// 分页回调
|
||||||
|
emit('change', page, pageSize);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-table-wrap">
|
||||||
|
<table class="m-table">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-tr">
|
||||||
|
<th
|
||||||
|
class="table-th"
|
||||||
|
:style="`width: ${typeof item.width === 'number' ? item.width + 'px' : item.width};`"
|
||||||
|
v-for="(item, index) in columns"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="m-table-body">
|
||||||
|
<tr class="table-loading" v-show="loading">
|
||||||
|
<Spin class="loading" size="small" :colspan="columns.length" v-bind="spinProps"/>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-empty-wrap" v-show="!dataSource.length">
|
||||||
|
<td class="table-empty" :colspan="columns.length">
|
||||||
|
<Empty class="empty" image="outlined" v-bind="emptyProps"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-tr" v-for="(data, index) in dataSource" :key="index">
|
||||||
|
<td class="m-td" v-for="(col, n) in columns" :key="n" :title="data[col.dataIndex as any]">
|
||||||
|
<slot v-if="col.slot" v-bind="data" :name="col.slot" :index="index">{{
|
||||||
|
data[col.dataIndex as any] || '--'
|
||||||
|
}}
|
||||||
|
</slot>
|
||||||
|
<span v-else>{{ data[col.dataIndex as any] || '--' }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<Pagination v-if="showPagination" class="mt16" @change="onChange" v-bind="pagination"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-table-wrap {
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
|
||||||
|
.m-table {
|
||||||
|
display: table;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.table-th {
|
||||||
|
padding: 16px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
background: #fafafa;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-table-body {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.table-loading {
|
||||||
|
border: none;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-wrap {
|
||||||
|
border: none;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty {
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
margin: 32px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-tr {
|
||||||
|
border: none;
|
||||||
|
background-color: #fff;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
|
||||||
|
.m-td {
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: background 0.3s;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt16 {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1067
src/components/MyUI/Tabs/Tabs.vue
Normal file
1067
src/components/MyUI/Tabs/Tabs.vue
Normal file
File diff suppressed because it is too large
Load Diff
620
src/components/MyUI/Tag/Tag.vue
Normal file
620
src/components/MyUI/Tag/Tag.vue
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, nextTick, ref, watchEffect} from 'vue';
|
||||||
|
import Space from '../Space/Space.vue';
|
||||||
|
import {useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
|
interface Tag {
|
||||||
|
label?: string // 标签文本名 string | slot
|
||||||
|
closable?: boolean // 标签是否可以关闭,默认 true
|
||||||
|
color?: string // 标签颜色
|
||||||
|
icon?: string // 设置图标 string | slot
|
||||||
|
size?: 'small' | 'middle' | 'large' // 标签尺寸
|
||||||
|
bordered?: boolean // 是否有边框,默认 true
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
closable?: boolean // 标签是否可以关闭
|
||||||
|
color?: string // 标签颜色
|
||||||
|
icon?: string // 设置图标 string | slot
|
||||||
|
size?: 'small' | 'middle' | 'large' // 标签尺寸
|
||||||
|
bordered?: boolean // 是否有边框
|
||||||
|
dynamic?: boolean // 是否启用标签动态添加和删除
|
||||||
|
spaceProps?: object // Space 组件属性配置,仅当 dynamic: true 时生效
|
||||||
|
value?: string[] | Tag[] // 动态标签数组,仅当 dynamic: true 时生效
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
closable: false,
|
||||||
|
color: undefined,
|
||||||
|
icon: undefined,
|
||||||
|
size: 'middle',
|
||||||
|
bordered: true,
|
||||||
|
dynamic: false,
|
||||||
|
spaceProps: () => ({}),
|
||||||
|
value: () => []
|
||||||
|
});
|
||||||
|
const inputRef = ref();
|
||||||
|
const showInput = ref(false);
|
||||||
|
const inputValue = ref('');
|
||||||
|
const presetColor = [
|
||||||
|
'success',
|
||||||
|
'processing',
|
||||||
|
'error',
|
||||||
|
'warning',
|
||||||
|
'default',
|
||||||
|
'pink',
|
||||||
|
'red',
|
||||||
|
'yellow',
|
||||||
|
'orange',
|
||||||
|
'cyan',
|
||||||
|
'green',
|
||||||
|
'blue',
|
||||||
|
'purple',
|
||||||
|
'geekblue',
|
||||||
|
'magenta',
|
||||||
|
'volcano',
|
||||||
|
'gold',
|
||||||
|
'lime'
|
||||||
|
];
|
||||||
|
const hidden = ref(false);
|
||||||
|
const tagsIconRef = ref();
|
||||||
|
const showTagsIcon = ref(Array(props.value.length).fill(1));
|
||||||
|
const slotsExist = useSlotsExist(['icon']);
|
||||||
|
const emits = defineEmits(['update:value', 'close', 'dynamicClose']);
|
||||||
|
const isStrArray = computed(() => {
|
||||||
|
if (props.dynamic) {
|
||||||
|
if (props.value.length) {
|
||||||
|
if (typeof props.value[0] === 'string') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof props.value[0] === 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const tags = computed(() => {
|
||||||
|
if (props.dynamic) {
|
||||||
|
if (props.value.length) {
|
||||||
|
if (isStrArray.value) {
|
||||||
|
return props.value.map((tag: any) => {
|
||||||
|
return {
|
||||||
|
label: tag,
|
||||||
|
closable: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return props.value.map((tag: any) => {
|
||||||
|
return {
|
||||||
|
closable: true,
|
||||||
|
...tag
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
const showIcon = computed(() => {
|
||||||
|
if (!props.dynamic) {
|
||||||
|
return slotsExist.icon || props.icon;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.dynamic) {
|
||||||
|
const len = props.value.length;
|
||||||
|
showTagsIcon.value = Array(len).fill(1);
|
||||||
|
nextTick(() => {
|
||||||
|
if (tagsIconRef.value) {
|
||||||
|
for (let n = 0; n < len; n++) {
|
||||||
|
showTagsIcon.value[n] = tagsIconRef.value[n].offsetWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onClose(e: MouseEvent) {
|
||||||
|
hidden.value = true;
|
||||||
|
emits('close', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCloseTags(tag: Tag, n: number) {
|
||||||
|
const newValue = (props.value as any[]).filter((_tag: any, index: number) => {
|
||||||
|
return index !== n;
|
||||||
|
});
|
||||||
|
emits('update:value', newValue);
|
||||||
|
emits('dynamicClose', tag, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAdd() {
|
||||||
|
showInput.value = true;
|
||||||
|
await nextTick();
|
||||||
|
inputRef.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange() {
|
||||||
|
if (isStrArray.value) {
|
||||||
|
emits('update:value', [...props.value, inputValue.value]);
|
||||||
|
} else {
|
||||||
|
emits('update:value', [
|
||||||
|
...props.value,
|
||||||
|
{
|
||||||
|
label: inputValue.value
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
showInput.value = false;
|
||||||
|
inputRef.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyboard(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
inputRef.value.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="!dynamic"
|
||||||
|
class="m-tag"
|
||||||
|
:class="[
|
||||||
|
`tag-${size}`,
|
||||||
|
color && presetColor.includes(color) ? `tag-${color}` : '',
|
||||||
|
{
|
||||||
|
'tag-borderless': !bordered,
|
||||||
|
'tag-has-color': color && !presetColor.includes(color),
|
||||||
|
'tag-hidden': hidden
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:style="`background-color: ${color && !presetColor.includes(color) ? color : ''};`"
|
||||||
|
>
|
||||||
|
<span v-if="showIcon" class="tag-icon">
|
||||||
|
<slot name="icon">{{ icon }}</slot>
|
||||||
|
</span>
|
||||||
|
<span class="tag-content">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
<span v-if="closable" class="tag-close" @click="onClose">
|
||||||
|
<svg
|
||||||
|
focusable="false"
|
||||||
|
class="close-svg"
|
||||||
|
data-icon="close"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Space v-else gap="small" v-bind="spaceProps">
|
||||||
|
<div
|
||||||
|
class="m-tag"
|
||||||
|
:class="[
|
||||||
|
`tag-${tag.size || size}`,
|
||||||
|
(tag.color || color) && presetColor.includes(tag.color || color) ? `tag-${tag.color || color}` : '',
|
||||||
|
{
|
||||||
|
'tag-borderless': tag.bordered !== undefined && !tag.bordered,
|
||||||
|
'tag-has-color': (tag.color || color) && !presetColor.includes(tag.color || color)
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:style="`background-color: ${(tag.color || color) && !presetColor.includes(tag.color || color) ? tag.color || color : ''};`"
|
||||||
|
v-for="(tag, index) in tags"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<span v-if="showTagsIcon[index]" ref="tagsIconRef" class="tag-icon">
|
||||||
|
<slot name="icon" :index="index">{{ tag.icon }}</slot>
|
||||||
|
</span>
|
||||||
|
<span class="tag-content">
|
||||||
|
<slot :label="tag.label" :index="index">{{ tag.label }}</slot>
|
||||||
|
</span>
|
||||||
|
<span v-if="tag.closable || closable" class="tag-close" @click="onCloseTags(tag, index)">
|
||||||
|
<svg
|
||||||
|
focusable="false"
|
||||||
|
class="close-svg"
|
||||||
|
data-icon="close"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!showInput" class="m-tag" :class="[`tag-${size}`, { 'tag-plus': dynamic }]" @click="onAdd">
|
||||||
|
<svg
|
||||||
|
focusable="false"
|
||||||
|
class="plus-svg"
|
||||||
|
data-icon="plus"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path d="M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"></path>
|
||||||
|
<path d="M176 474h672q8 0 8 8v60q0 8-8 8H176q-8 0-8-8v-60q0-8 8-8z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-if="showInput"
|
||||||
|
ref="inputRef"
|
||||||
|
class="tag-input"
|
||||||
|
:class="`input-${size}`"
|
||||||
|
type="text"
|
||||||
|
v-model="inputValue"
|
||||||
|
@blur="showInput = false"
|
||||||
|
@change="onChange"
|
||||||
|
@keydown="onKeyboard"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
padding-inline: 7px;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-align: start;
|
||||||
|
|
||||||
|
.tag-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-content {
|
||||||
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-svg {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
fill: currentColor;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 0;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: -0.175em;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-close {
|
||||||
|
margin-left: 3px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.close-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
fill: currentColor;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-small {
|
||||||
|
height: 22px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.plus-svg {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-close {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-large {
|
||||||
|
height: 28px;
|
||||||
|
line-height: 26px;
|
||||||
|
|
||||||
|
.tag-close {
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: -0.16em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-plus {
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
border-style: dashed;
|
||||||
|
padding-inline: 10px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: @themeColor;
|
||||||
|
|
||||||
|
.plus-svg {
|
||||||
|
color: @themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input {
|
||||||
|
width: 86px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
padding: 0 8px;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #4096ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
||||||
|
border-right-width: 1px;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-small {
|
||||||
|
width: 78px;
|
||||||
|
height: 22px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-large {
|
||||||
|
width: 90px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-success {
|
||||||
|
color: #52c41a;
|
||||||
|
background: #f6ffed;
|
||||||
|
border-color: #b7eb8f;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #52c41a;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-processing {
|
||||||
|
color: @themeColor;
|
||||||
|
background: #e6f4ff;
|
||||||
|
border-color: #91caff;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: @themeColor;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
background: #fff2f0;
|
||||||
|
border-color: #ffccc7;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #ff4d4f;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-warning {
|
||||||
|
color: #faad14;
|
||||||
|
background: #fffbe6;
|
||||||
|
border-color: #ffe58f;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #faad14;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-pink {
|
||||||
|
color: #c41d7f;
|
||||||
|
background: #fff0f6;
|
||||||
|
border-color: #ffadd2;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #c41d7f;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-red {
|
||||||
|
color: #cf1322;
|
||||||
|
background: #fff1f0;
|
||||||
|
border-color: #ffa39e;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #cf1322;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-yellow {
|
||||||
|
color: #d4b106;
|
||||||
|
background: #feffe6;
|
||||||
|
border-color: #fffb8f;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #d4b106;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-orange {
|
||||||
|
color: #d46b08;
|
||||||
|
background: #fff7e6;
|
||||||
|
border-color: #ffd591;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #d46b08;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-green {
|
||||||
|
color: #389e0d;
|
||||||
|
background: #f6ffed;
|
||||||
|
border-color: #b7eb8f;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #389e0d;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cyan {
|
||||||
|
color: #08979c;
|
||||||
|
background: #e6fffb;
|
||||||
|
border-color: #87e8de;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #08979c;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-blue {
|
||||||
|
color: #0958d9;
|
||||||
|
background: #e6f4ff;
|
||||||
|
border-color: #91caff;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #0958d9;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-purple {
|
||||||
|
color: #531dab;
|
||||||
|
background: #f9f0ff;
|
||||||
|
border-color: #d3adf7;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #531dab;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-geekblue {
|
||||||
|
color: #1d39c4;
|
||||||
|
background: #f0f5ff;
|
||||||
|
border-color: #adc6ff;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #1d39c4;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-magenta {
|
||||||
|
color: #eb2f96;
|
||||||
|
background: #fff0f6;
|
||||||
|
border-color: #ffadd2;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #eb2f96;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-volcano {
|
||||||
|
color: #d4380d;
|
||||||
|
background: #fff2e8;
|
||||||
|
border-color: #ffbb96;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #d4380d;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-gold {
|
||||||
|
color: #d48806;
|
||||||
|
background: #fffbe6;
|
||||||
|
border-color: #ffe58f;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #d48806;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-lime {
|
||||||
|
color: #7cb305;
|
||||||
|
background: #fcffe6;
|
||||||
|
border-color: #eaff8f;
|
||||||
|
|
||||||
|
:deep(svg) {
|
||||||
|
color: #7cb305;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-borderless {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-has-color {
|
||||||
|
color: #fff;
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
|
.tag-close .close-svg {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
336
src/components/MyUI/TextScroll/TextScroll.vue
Normal file
336
src/components/MyUI/TextScroll/TextScroll.vue
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {CSSProperties} from 'vue';
|
||||||
|
import {computed, ref, watch, watchEffect} from 'vue';
|
||||||
|
import {cancelRaf, rafTimeout, useResizeObserver} from '../Utils';
|
||||||
|
|
||||||
|
interface Text {
|
||||||
|
title: string // 文字标题
|
||||||
|
href?: string // 跳转链接
|
||||||
|
target?: '_self' | '_blank' // 跳转链接打开方式,href 存在时生效
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
scrollText?: Text[] | Text // 滚动文字数组,single 为 true 时,类型为 Text;多条文字滚动时,数组长度必须大于等于 amount 才能滚动
|
||||||
|
single?: boolean // 是否启用单条文字滚动效果,只支持水平文字滚动,为 true 时,amount 自动设为 1
|
||||||
|
width?: number | string // 滚动区域宽度,单位 px
|
||||||
|
height?: number // 滚动区域高度,单位 px
|
||||||
|
boardStyle?: CSSProperties // 滚动区域样式,优先级低于 width、height
|
||||||
|
textStyle?: CSSProperties // 滚动文字样式
|
||||||
|
hrefHoverColor?: string // 链接文字鼠标悬浮颜色;仅当 href 存在时生效
|
||||||
|
amount?: number // 滚动区域展示条数,水平滚动时生效
|
||||||
|
gap?: number // 水平滚动文字各列间距或垂直滚动文字两边的边距,单位 px
|
||||||
|
interval?: number // 水平滚动动画执行时间间隔,单位 ms,水平滚动时生效
|
||||||
|
step?: number // 水平滚动动画每次执行时移动距离,单位 px,水平滚动时生效,与 interval 配合控制滚动速度
|
||||||
|
vertical?: boolean // 是否垂直滚动
|
||||||
|
verticalInterval?: number // 垂直文字滚动时间间隔,单位 ms,垂直滚动时生效
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
scrollText: () => [],
|
||||||
|
single: false,
|
||||||
|
width: '100%',
|
||||||
|
height: 50,
|
||||||
|
boardStyle: () => ({}),
|
||||||
|
textStyle: () => ({}),
|
||||||
|
hrefHoverColor: '#1677ff',
|
||||||
|
amount: 4,
|
||||||
|
gap: 20,
|
||||||
|
interval: 10,
|
||||||
|
step: 1,
|
||||||
|
vertical: false,
|
||||||
|
verticalInterval: 3000
|
||||||
|
});
|
||||||
|
const horizontalRef = ref(); // 水平滚动 DOM 引用
|
||||||
|
const verticalRef = ref(); // 垂直滚动 DOM 引用
|
||||||
|
const left = ref(0); // 水平滚动偏移距离
|
||||||
|
const distance = ref(0); // 每条滚动文字移动距离
|
||||||
|
const activeIndex = ref(0); // 垂直滚动当前索引
|
||||||
|
const horizontalMoveRaf = ref();
|
||||||
|
const verticalMoveRaf = ref();
|
||||||
|
const origin = ref(true); // 垂直滚动初始状态
|
||||||
|
const emit = defineEmits(['click']);
|
||||||
|
const textData = ref<Text[]>([]);
|
||||||
|
const textAmount = computed(() => {
|
||||||
|
return textData.value.length;
|
||||||
|
});
|
||||||
|
const totalWidth = computed(() => {
|
||||||
|
// 文字滚动区域总宽度
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const displayAmount = computed(() => {
|
||||||
|
if (props.single) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return props.amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
textData.value,
|
||||||
|
props.width,
|
||||||
|
props.amount,
|
||||||
|
props.gap,
|
||||||
|
props.step,
|
||||||
|
props.interval,
|
||||||
|
props.vertical,
|
||||||
|
props.verticalInterval
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
initScroll();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true, // 强制转成深层侦听器
|
||||||
|
flush: 'post'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watchEffect(() => {
|
||||||
|
initScrollText();
|
||||||
|
});
|
||||||
|
useResizeObserver([horizontalRef, verticalRef], () => {
|
||||||
|
initScroll();
|
||||||
|
});
|
||||||
|
|
||||||
|
function initScrollText() {
|
||||||
|
left.value = 0;
|
||||||
|
activeIndex.value = 0;
|
||||||
|
if (props.single) {
|
||||||
|
textData.value = [props.scrollText, props.scrollText] as Text[];
|
||||||
|
} else {
|
||||||
|
const text = props.scrollText as Text[];
|
||||||
|
if (text.length === props.amount) {
|
||||||
|
textData.value = [...text, ...text];
|
||||||
|
} else {
|
||||||
|
textData.value = [...text];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initScroll() {
|
||||||
|
if (!props.vertical) {
|
||||||
|
distance.value = getDistance(); // 获取每列文字宽度
|
||||||
|
} else {
|
||||||
|
origin.value = true;
|
||||||
|
}
|
||||||
|
cancelRaf(horizontalMoveRaf.value);
|
||||||
|
cancelRaf(verticalMoveRaf.value);
|
||||||
|
startMove(); // 开始滚动
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDistance(): number {
|
||||||
|
return parseFloat((horizontalRef.value.offsetWidth / displayAmount.value).toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMove() {
|
||||||
|
if (props.vertical) {
|
||||||
|
if (textAmount.value > 1) {
|
||||||
|
cancelRaf(verticalMoveRaf.value);
|
||||||
|
verticalMove(); // 垂直滚动
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (textAmount.value > displayAmount.value) {
|
||||||
|
// 超过 amount 条开始滚动
|
||||||
|
cancelRaf(horizontalMoveRaf.value);
|
||||||
|
horizonMove(); // 水平滚动
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopMove() {
|
||||||
|
// 暂停动画
|
||||||
|
if (props.vertical) {
|
||||||
|
cancelRaf(verticalMoveRaf.value);
|
||||||
|
} else {
|
||||||
|
cancelRaf(horizontalMoveRaf.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function horizonMove() {
|
||||||
|
horizontalMoveRaf.value = rafTimeout(
|
||||||
|
() => {
|
||||||
|
if (left.value >= distance.value) {
|
||||||
|
textData.value.push(textData.value.shift() as Text); // 将第一条数据放到最后
|
||||||
|
left.value = 0;
|
||||||
|
} else {
|
||||||
|
left.value += props.step; // 每次移动step(px)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props.interval,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verticalMove() {
|
||||||
|
verticalMoveRaf.value = rafTimeout(
|
||||||
|
() => {
|
||||||
|
if (origin.value) {
|
||||||
|
origin.value = false;
|
||||||
|
}
|
||||||
|
activeIndex.value = (activeIndex.value + 1) % textAmount.value;
|
||||||
|
verticalMove();
|
||||||
|
},
|
||||||
|
origin.value ? props.verticalInterval : props.verticalInterval + 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(text: Text) {
|
||||||
|
// 通知父组件点击的标题
|
||||||
|
emit('click', text);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
start: startMove,
|
||||||
|
stop: stopMove,
|
||||||
|
reset: initScrollText
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="!vertical"
|
||||||
|
ref="horizontalRef"
|
||||||
|
class="m-slider-horizontal"
|
||||||
|
:style="[
|
||||||
|
boardStyle,
|
||||||
|
`--href-hover-color: ${hrefHoverColor}; --text-gap: ${gap}px; height: ${height}px; width: ${totalWidth};`
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="m-scroll-view" :style="`will-change: transform; transform: translateX(${-left}px);`">
|
||||||
|
<component
|
||||||
|
:is="text.href ? 'a' : 'div'"
|
||||||
|
class="slide-text"
|
||||||
|
:class="{ 'href-text': text.href }"
|
||||||
|
:style="[textStyle, `width: ${distance}px;`]"
|
||||||
|
v-for="(text, index) in <Text[]>textData"
|
||||||
|
:key="index"
|
||||||
|
:title="text.title"
|
||||||
|
:href="text.href"
|
||||||
|
:target="text.target"
|
||||||
|
@mouseenter="stopMove"
|
||||||
|
@mouseleave="startMove"
|
||||||
|
@click="onClick(text)"
|
||||||
|
>
|
||||||
|
{{ text.title || '--' }}
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
ref="verticalRef"
|
||||||
|
class="m-slider-vertical"
|
||||||
|
:style="[
|
||||||
|
boardStyle,
|
||||||
|
`--href-hover-color: ${hrefHoverColor}; --enter-move: ${height}px; --leave-move: ${-height}px; --tex-gap: ${gap}px; height: ${height}px; width: ${totalWidth};`
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<TransitionGroup name="slide">
|
||||||
|
<div class="m-scroll-view" v-for="(text, index) in <Text[]>textData" :key="index" v-show="activeIndex === index">
|
||||||
|
<component
|
||||||
|
:is="text.href ? 'a' : 'div'"
|
||||||
|
class="slide-text"
|
||||||
|
:class="{ 'href-text': text.href }"
|
||||||
|
:style="textStyle"
|
||||||
|
:title="text.title"
|
||||||
|
:href="text.href"
|
||||||
|
:target="text.target"
|
||||||
|
@mouseenter="stopMove"
|
||||||
|
@mouseleave="startMove"
|
||||||
|
@click="onClick(text)"
|
||||||
|
>
|
||||||
|
{{ text.title || '--' }}
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
// 水平滚动
|
||||||
|
.m-slider-horizontal {
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0px 0px 5px #d3d3d3;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.m-scroll-view {
|
||||||
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.slide-text {
|
||||||
|
padding-left: var(--text-gap);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.57;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.href-text {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--href-hover-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 垂直滚动
|
||||||
|
.slide-enter-active,
|
||||||
|
.slide-leave-active {
|
||||||
|
transition: all 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-enter-from {
|
||||||
|
transform: translateY(var(--enter-move)) scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-leave-to {
|
||||||
|
transform: translateY(var(--leave-move)) scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-slider-vertical {
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0px 0px 5px #d3d3d3;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.m-scroll-view {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 var(--tex-gap);
|
||||||
|
|
||||||
|
.slide-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.57;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.href-text {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--href-hover-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
185
src/components/MyUI/Timeline/Timeline.vue
Normal file
185
src/components/MyUI/Timeline/Timeline.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watchEffect } from 'vue';
|
||||||
|
interface Data {
|
||||||
|
desc: string // 文字描述 string | slot
|
||||||
|
color?: 'blue' | 'green' | 'red' | 'gray' | string // 圆圈颜色,默认值 blue
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
timelineData?: Data[] // 时间轴内容数组
|
||||||
|
width?: number | string // 时间轴区域总宽度,单位 px
|
||||||
|
lineStyle?: 'solid' | 'dashed' | 'dotted' // 时间线样式
|
||||||
|
mode?: 'left' | 'center' | 'right' // 通过设置 mode 可以改变时间轴和内容的相对位置
|
||||||
|
position?: 'left' | 'right' // 当 mode 为 center 时,内容交替展现,内容从左边(left)开始或者右边(right)开始展现
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
timelineData: () => [],
|
||||||
|
width: '100%',
|
||||||
|
lineStyle: 'solid',
|
||||||
|
mode: 'left',
|
||||||
|
position: 'left'
|
||||||
|
});
|
||||||
|
enum ColorStyle { // 颜色主题对象
|
||||||
|
blue = '#1677ff',
|
||||||
|
green = '#52c41a',
|
||||||
|
red = '#ff4d4f',
|
||||||
|
gray = '#00000040'
|
||||||
|
}
|
||||||
|
const desc = ref();
|
||||||
|
const dotsHeight = ref<string[]>([]);
|
||||||
|
const totalWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const len = computed(() => {
|
||||||
|
return props.timelineData.length;
|
||||||
|
});
|
||||||
|
function getDotsHeight() {
|
||||||
|
for (let n = 0; n < len.value; n++) {
|
||||||
|
dotsHeight.value[n] = getComputedStyle(desc.value[n].firstElementChild || desc.value[n], null).getPropertyValue(
|
||||||
|
'line-height'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
getDotsHeight();
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
);
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
if (props.mode === 'center') {
|
||||||
|
for (let n = 0; n < len.value; n++) {
|
||||||
|
if ((n + 1) % 2) {
|
||||||
|
// odd
|
||||||
|
if (props.position === 'left') {
|
||||||
|
desc.value[n].classList.add('desc-alternate-left');
|
||||||
|
} else {
|
||||||
|
desc.value[n].classList.add('desc-alternate-right');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// even
|
||||||
|
if (props.position === 'left') {
|
||||||
|
desc.value[n].classList.add('desc-alternate-right');
|
||||||
|
} else {
|
||||||
|
desc.value[n].classList.add('desc-alternate-left');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-timeline" :style="`width: ${totalWidth};`">
|
||||||
|
<div
|
||||||
|
class="timeline-item"
|
||||||
|
:class="{ 'item-last': index === timelineData.length - 1 }"
|
||||||
|
v-for="(data, index) in timelineData"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<span class="timeline-tail" :class="`tail-${mode}`" :style="`border-left-style: ${lineStyle};`"></span>
|
||||||
|
<div class="timeline-dot" :class="`dot-${mode}`" :style="`height: ${dotsHeight[index]}`">
|
||||||
|
<slot name="dot" :index="index">
|
||||||
|
<span class="dot-item" v-if="data.color === 'red'" :style="{ borderColor: ColorStyle.red }"></span>
|
||||||
|
<span class="dot-item" v-else-if="data.color === 'gray'" :style="{ borderColor: ColorStyle.gray }"></span>
|
||||||
|
<span class="dot-item" v-else-if="data.color === 'green'" :style="{ borderColor: ColorStyle.green }"></span>
|
||||||
|
<span class="dot-item" v-else-if="data.color === 'blue'" :style="{ borderColor: ColorStyle.blue }"></span>
|
||||||
|
<span class="dot-item" v-else :style="{ borderColor: data.color || ColorStyle.blue }"></span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div ref="desc" :class="`timeline-desc desc-${mode}`">
|
||||||
|
<slot name="desc" :index="index">{{ data.desc || '--' }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-timeline {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
.timeline-tail {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-left-width: 2px;
|
||||||
|
border-left-color: #e8e8e8;
|
||||||
|
}
|
||||||
|
.tail-left {
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
.tail-center {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.tail-right {
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
:deep(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
.dot-item {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dot-left {
|
||||||
|
left: 6px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.dot-center {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.dot-right {
|
||||||
|
right: 6px;
|
||||||
|
transform: translateX(50%);
|
||||||
|
}
|
||||||
|
.timeline-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.desc-left {
|
||||||
|
margin-left: 25px;
|
||||||
|
}
|
||||||
|
.desc-center {
|
||||||
|
width: calc(50% - 12px);
|
||||||
|
}
|
||||||
|
.desc-alternate-left {
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
.desc-alternate-right {
|
||||||
|
margin-left: calc(50% + 12px);
|
||||||
|
}
|
||||||
|
.desc-right {
|
||||||
|
margin-right: 25px;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.item-last {
|
||||||
|
.timeline-tail {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {CSSProperties} from 'vue';
|
import type {CSSProperties} from 'vue';
|
||||||
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
||||||
import {cancelRaf, rafTimeout, useEventListener, useSlotsExist} from '../utils';
|
import {cancelRaf, rafTimeout, useEventListener, useSlotsExist} from '../Utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
maxWidth?: string | number // 弹出提示最大宽度,单位 px
|
maxWidth?: string | number // 弹出提示最大宽度,单位 px
|
||||||
|
|||||||
285
src/components/MyUI/TreeChart/TreeChart.vue
Normal file
285
src/components/MyUI/TreeChart/TreeChart.vue
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, onMounted, ref, watch} from 'vue';
|
||||||
|
/*
|
||||||
|
按需引入
|
||||||
|
*/
|
||||||
|
// 使用 ECharts 提供的按需引入的接口来打包必须的组件
|
||||||
|
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
// 引入树图图表,图表后缀都为 Chart
|
||||||
|
import {TreeChart} from 'echarts/charts';
|
||||||
|
// 引入提示框,组件后缀都为 Component
|
||||||
|
import {TooltipComponent} from 'echarts/components';
|
||||||
|
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
|
||||||
|
import {CanvasRenderer} from 'echarts/renderers';
|
||||||
|
// 注册必须的组件
|
||||||
|
echarts.use([TreeChart, TooltipComponent, CanvasRenderer]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
全部引入
|
||||||
|
*/
|
||||||
|
// import * as echarts from 'echarts'
|
||||||
|
/*
|
||||||
|
需要注意的是为了保证打包的体积是最小的,ECharts 按需引入的时候不再提供任何渲染器,
|
||||||
|
所以需要选择引入 CanvasRenderer 或者 SVGRenderer 作为渲染器。这样的好处是假如
|
||||||
|
你只需要使用 svg 渲染模式,打包的结果中就不会再包含无需使用的 CanvasRenderer 模块
|
||||||
|
*/
|
||||||
|
const chart = ref();
|
||||||
|
const treeChart = ref();
|
||||||
|
var option: any;
|
||||||
|
|
||||||
|
interface Tree {
|
||||||
|
name: string // 数据项名称
|
||||||
|
value?: number // 数据值
|
||||||
|
[propName: string]: any // 添加一个字符串索引签名,用于包含带有任意数量的其他属性
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
treeData: Tree[] // 树图数据源
|
||||||
|
width?: string | number // 容器宽度
|
||||||
|
height?: string | number // 容器高度
|
||||||
|
themeColor?: string // 主题色
|
||||||
|
edgeShape?: 'curve' | 'polyline' // 树图边的形状,有曲线curve和折线polyline两种,只有正交布局下生效
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
treeData: () => ([]),
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
themeColor: '#1677FF',
|
||||||
|
edgeShape: 'curve'
|
||||||
|
});
|
||||||
|
const chartWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return props.width + 'px';
|
||||||
|
} else {
|
||||||
|
return props.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const chartHeight = computed(() => {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return props.height + 'px';
|
||||||
|
} else {
|
||||||
|
return props.height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
initChart(); // 初始化图标示例
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.treeData,
|
||||||
|
(to) => {
|
||||||
|
// 监听并更新图例数据
|
||||||
|
option.series[0].data = to;
|
||||||
|
treeChart.value.setOption(option);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => [props.width, props.height, props.themeColor, props.edgeShape],
|
||||||
|
() => {
|
||||||
|
if (treeChart.value) {
|
||||||
|
treeChart.value.dispose(); // 销毁实例
|
||||||
|
}
|
||||||
|
initChart(); // 重新初始化实例
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
flush: 'post'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// const loadingConfig = {
|
||||||
|
// text: 'loading',
|
||||||
|
// color: '#c23531',
|
||||||
|
// textColor: '#000',
|
||||||
|
// maskColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
// zlevel: 0,
|
||||||
|
// 字体大小。从 `v4.8.0` 开始支持。
|
||||||
|
// fontSize: 12,
|
||||||
|
// 是否显示旋转动画(spinner)。从 `v4.8.0` 开始支持。
|
||||||
|
// showSpinner: true,
|
||||||
|
// 旋转动画(spinner)的半径。从 `v4.8.0` 开始支持。
|
||||||
|
// spinnerRadius: 20,
|
||||||
|
// 旋转动画(spinner)的线宽。从 `v4.8.0` 开始支持。
|
||||||
|
// lineWidth: 5,
|
||||||
|
// 字体粗细。从 `v5.0.1` 开始支持。
|
||||||
|
// fontWeight: 'normal',
|
||||||
|
// 字体风格。从 `v5.0.1` 开始支持。
|
||||||
|
// fontStyle: 'normal',
|
||||||
|
// 字体系列。从 `v5.0.1` 开始支持。
|
||||||
|
// fontFamily: 'sans-serif'
|
||||||
|
// }
|
||||||
|
function showLoading(config: any) {
|
||||||
|
treeChart.value.showLoading('default', {text: '', color: props.themeColor, ...config}); // 显示加载动画效果
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading() {
|
||||||
|
treeChart.value.hideLoading(); // 隐藏动画加载效果
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
showLoading,
|
||||||
|
hideLoading
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['clickNode']);
|
||||||
|
|
||||||
|
function onClick(e: any) {
|
||||||
|
emit('clickNode', e.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initChart() {
|
||||||
|
// 等价于使用 Canvas 渲染器(默认):echarts.init(containerDom, null, { renderer: 'canvas' })
|
||||||
|
treeChart.value = echarts.init(chart.value);
|
||||||
|
option = {
|
||||||
|
tooltip: { // 提示框浮层设置
|
||||||
|
trigger: 'item',
|
||||||
|
triggerOn: 'mousemove', // 提示框触发条件
|
||||||
|
enterable: true, // 鼠标是否可进入提示框浮层中,默认false
|
||||||
|
confine: true, // 是否将tooltip框限制在图表的区域内
|
||||||
|
formatter: function (params: any) { // 提示框浮层内容格式器,支持字符串模板和回调函数两种形式
|
||||||
|
// console.log('params:', params)
|
||||||
|
return params.marker + params.name + '<br/>' + '$ ' + (params.value || '--');
|
||||||
|
},
|
||||||
|
// valueFormatter: function (value) { // tooltip 中数值显示部分的格式化回调函数
|
||||||
|
// return '$' + value.toFixed(2)
|
||||||
|
// },
|
||||||
|
backgroundColor: '#FFF', // 提示框浮层的背景颜色
|
||||||
|
borderColor: props.themeColor, // 提示框浮层的边框颜色
|
||||||
|
borderWidth: 1, // 提示框浮层的边框宽
|
||||||
|
borderRadius: 8, // 提示框浮层圆角
|
||||||
|
padding: [6, 8], // 提示框浮层的内边距
|
||||||
|
textStyle: { // 提示框浮层的文本样式
|
||||||
|
color: '#333', // 文字颜色
|
||||||
|
fontWeight: 400, // 字体粗细
|
||||||
|
fontSize: 16, // 字体大小
|
||||||
|
lineHeight: 20, // 行高
|
||||||
|
width: 60, // 文本显示宽度
|
||||||
|
// 文字超出宽度是否截断或者换行;只有配置width时有效
|
||||||
|
overflow: 'breakAll', // truncate截断,并在末尾显示ellipsis配置的文本,默认为...;break换行;breakAll换行,并强制单词内换行
|
||||||
|
ellipsis: '...'
|
||||||
|
},
|
||||||
|
extraCssText: 'box-shadow: 0 0 9px rgba(0, 0, 0, 0.3); text-align: right;' // 额外添加到浮层的css样式
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'tree',
|
||||||
|
data: props.treeData,
|
||||||
|
name: '树图',
|
||||||
|
top: '1%', // 组件离容器上侧的距离,像素值20,或相对容器的百分比20%
|
||||||
|
left: '10%', // 组件离容器左侧的距离
|
||||||
|
bottom: '1%', // 组件离容器下侧的距离
|
||||||
|
right: '16%', // 组件离容器右侧的距离
|
||||||
|
layout: 'orthogonal', // 树图的布局,正交orthogonal和径向radial两种
|
||||||
|
orient: 'LR', // 树图中正交布局的方向,'LR','RL','TB','BT',只有布局是正交时才生效
|
||||||
|
edgeShape: props.edgeShape, // 树图边的形状,有曲线curve和折线polyline两种,只有正交布局下生效
|
||||||
|
roam: false, // 是否开启鼠标缩放或平移,默认false
|
||||||
|
initialTreeDepth: 2, // 树图初始的展开层级(深度),根节点是0,不设置时全部展开
|
||||||
|
// symbol: 'arrow', // 标记的图形,默认是emptyCircle;circle,rect,roundRect,triangle,diamond,pin,arrow,none
|
||||||
|
// symbolRotate: 270, // 配合arrow图形使用效果较好
|
||||||
|
symbolSize: 16, // 大于0时是圆圈,等于0时不展示,标记的大小
|
||||||
|
itemStyle: { // 树图中每个节点的样式
|
||||||
|
color: props.themeColor, // 节点未展开时的填充色
|
||||||
|
borderColor: 'rgba(255, 144, 0, 1)', // 图形的描边颜色
|
||||||
|
borderWidth: 1, // 描边线宽,为0时无描边
|
||||||
|
borderType: 'dotted', // 描边类型
|
||||||
|
borderCap: 'square', // 指定线段末端的绘制方式butt方形结束,round圆形结束,square
|
||||||
|
shadowColor: 'rgba(0,121,221,0.3)', // 阴影颜色
|
||||||
|
shadowBlur: 16, // 图形阴影的模糊大小
|
||||||
|
opacity: 1 // 图形透明度
|
||||||
|
},
|
||||||
|
label: { // 每个节点对应的文本标签样式
|
||||||
|
show: true, // 是否显示标签
|
||||||
|
distance: 8, // 文本距离图形元素的距离
|
||||||
|
position: 'left', // 标签位置
|
||||||
|
verticalAlign: 'middle', // 文字垂直对齐方式,默认自动,top,middle,bottom
|
||||||
|
align: 'center', // 文字水平对齐方式,默认自动,left,right,center
|
||||||
|
fontSize: 16, // 字体大小
|
||||||
|
color: '#333', // 字体颜色
|
||||||
|
backgroundColor: '#F0F5FA', // 文字块的背景颜色
|
||||||
|
borderColor: props.themeColor, // 文字块边框颜色
|
||||||
|
borderWidth: 1, // 文字块边框宽度
|
||||||
|
borderType: 'solid', // 文字块边框描边类型 solid dashed dotted
|
||||||
|
borderRadius: 4, // 文字块的圆角
|
||||||
|
padding: [6, 12], // 文字块内边距
|
||||||
|
shadowColor: 'rgba(0,121,221,0.3)', // 文字块的背景阴影颜色
|
||||||
|
shadowBlur: 6, // 文字块的背景阴影长度
|
||||||
|
width: 60,
|
||||||
|
// 文字超出宽度是否截断或者换行;只有配置width时有效
|
||||||
|
overflow: 'truncate', // truncate截断,并在末尾显示ellipsis配置的文本,默认为...;break换行;breakAll换行,并强制单词内换行
|
||||||
|
ellipsis: '...'
|
||||||
|
},
|
||||||
|
lineStyle: { // 树图边的样式
|
||||||
|
color: 'rgba(0,0,0,.35)', // 树图边的颜色
|
||||||
|
width: 2, // 树图边的宽度
|
||||||
|
curveness: 0.5, // 树图边的曲度
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)', // 阴影颜色
|
||||||
|
shadowBlur: 10 // 图形阴影的模糊大小
|
||||||
|
},
|
||||||
|
emphasis: { // 树图中图形和标签高亮的样式
|
||||||
|
disabled: false, // 是否关闭高亮状态,默认false
|
||||||
|
// 在高亮图形时,是否淡出其它数据的图形已达到聚焦的效果
|
||||||
|
focus: 'self', // none不淡出其他图形(默认);self只聚焦当前高亮的数据图形;series聚焦当前高亮的数据所在系列的所有图形;ancestor聚焦所有祖先节点;descendant聚焦所有子孙节点;relative聚焦所有子孙和祖先节点
|
||||||
|
blurScope: 'coordinateSystem', // 开启focus时,配置淡出的范围,coordinateSystem淡出范围为坐标系(默认);series淡出范围为系列;global淡出范围为全局
|
||||||
|
itemStyle: { // 该节点的样式
|
||||||
|
color: props.themeColor, // 图形的颜色
|
||||||
|
// borderColor: 'rgba(255, 144, 0, 1)', // 图形的描边颜色
|
||||||
|
borderWidth: 1, // 描边线宽,为0时无描边
|
||||||
|
borderType: 'solid', // 描边类型 solid dashed dotted
|
||||||
|
borderCap: 'square', // 指定线段末端的绘制方式butt方形结束,round圆形结束,square
|
||||||
|
shadowColor: 'rgba(0,121,221,0.3)', // 阴影颜色
|
||||||
|
shadowBlur: 12, // 图形阴影的模糊大小
|
||||||
|
opacity: 1 // 图形透明度
|
||||||
|
},
|
||||||
|
lineStyle: { // 树图边的样式
|
||||||
|
color: 'rgba(0,0,0,.45)', // 树图边的颜色
|
||||||
|
width: 2, // 树图边的宽度
|
||||||
|
curveness: 0.5, // 树图边的曲度
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)', // 阴影颜色
|
||||||
|
shadowBlur: 6 // 图形阴影的模糊大小
|
||||||
|
},
|
||||||
|
label: { // 高亮标签的文本样式
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: 600
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blur: { // 淡出状态的相关配置,开启emphasis.focus后有效
|
||||||
|
itemStyle: {}, // 节点的样式
|
||||||
|
lineStyle: {}, // 树图边的样式
|
||||||
|
label: {} // 淡出标签的文本样式
|
||||||
|
},
|
||||||
|
leaves: { // 叶子节点的特殊配置
|
||||||
|
label: { // 叶子节点的文本标签样式
|
||||||
|
distance: 8,
|
||||||
|
// color: props.themeColor,
|
||||||
|
position: 'right',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
align: 'left'
|
||||||
|
},
|
||||||
|
itemStyle: {}, // 叶子节点的样式
|
||||||
|
emphasis: {}, // 叶子节点高亮状态的配置
|
||||||
|
blur: {}, // 叶子节点淡出状态的配置
|
||||||
|
select: {} // 叶子节点选中状态的配置
|
||||||
|
},
|
||||||
|
animation: true, // 是否开启动画
|
||||||
|
expandAndCollapse: true, // 子树折叠和展开的交互,默认打开
|
||||||
|
animationDuration: 550, // 初始动画的时长
|
||||||
|
animationEasing: 'linear', // 初始动画的缓动效果
|
||||||
|
animationDelay: 0, // 初始动画的延迟
|
||||||
|
animationDurationUpdate: 750, // 数据更新动画的时长
|
||||||
|
animationEasingUpdate: 'cubicInOut', // 数据更新动画的缓动效果
|
||||||
|
animationDelayUpdate: 0 // 数据更新动画的延迟
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
if (option) {
|
||||||
|
treeChart.value.setOption(option);
|
||||||
|
}
|
||||||
|
// 监听树图节点的点击事件
|
||||||
|
treeChart.value.on('click', onClick);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div ref="chart" :style="`width: ${chartWidth}; height: ${chartHeight};`"></div>
|
||||||
|
</template>
|
||||||
590
src/components/MyUI/Upload/Upload.vue
Normal file
590
src/components/MyUI/Upload/Upload.vue
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, watchEffect} from 'vue';
|
||||||
|
// import Spin from '../Spin/Spin.vue';
|
||||||
|
// import Message from '../Message/Message.vue';
|
||||||
|
// import Image from '../Image/Image.vue';
|
||||||
|
// import Space from '../Space/Space.vue';
|
||||||
|
|
||||||
|
interface FileType {
|
||||||
|
name?: string // 文件名
|
||||||
|
url: any // 文件地址
|
||||||
|
[propName: string]: any // 添加一个字符串索引签名,用于包含带有任意数量的其他属性
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageType {
|
||||||
|
upload?: string // 上传成功的消息提示,没有设置该属性时即不显示上传消息提示
|
||||||
|
remove?: string // 删除成功的消息提示,没有设置该属性时即不显示删除消息提示
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
accept?: string // 接受上传的文件类型,与 <input type="file" /> 的 accept 属性一致,参考 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes/accept
|
||||||
|
multiple?: boolean // 是否支持多选文件,开启后可选择多个文件
|
||||||
|
maxCount?: number // 限制上传数量。当为 1 时,始终用最新上传的文件代替当前文件
|
||||||
|
tip?: string // 上传描述文字 string | slot
|
||||||
|
fit?: 'contain' | 'fill' | 'cover' | 'none' | 'scale-down' // 预览图片缩放规则,仅当上传文件为图片时生效
|
||||||
|
draggable?: boolean // 是否支持拖拽上传,开启后可拖拽文件到选择框上传
|
||||||
|
disabled?: boolean // 是否禁用,只能预览,不能删除和上传
|
||||||
|
spaceProps?: object // Space 组件属性配置,用于配置多个文件时的排列方式
|
||||||
|
spinProps?: object // Spin 组件属性配置,用于配置上传中样式
|
||||||
|
imageProps?: object // Image 组件属性配置,用于配置图片预览
|
||||||
|
messageProps?: object // Message 组件属性配置,用于配置操作消息提示
|
||||||
|
actionMessage?: MessageType // 操作完成的消息提示,传 {} 即可不显示任何消息提示
|
||||||
|
beforeUpload?: any // 上传文件之前的钩子,参数为上传的文件,返回 false 则停止上传,返回 true 开始上传;支持返回一个 Promise 对象(如服务端校验等),Promise 对象 reject 时停止上传,resolve 时开始上传;通常用来校验用户上传的文件格式和大小
|
||||||
|
uploadMode?: 'base64' | 'custom' // 上传文件的方式,默认是 base64,可选 'base64' | 'custom'
|
||||||
|
customRequest?: any // 自定义上传行为,只有 uploadMode: custom 时,才会使用 customRequest 自定义上传行为
|
||||||
|
fileList?: FileType[] // (v-model) 已上传的文件列表
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
accept: '*', // 默认支持所有类型
|
||||||
|
multiple: false,
|
||||||
|
maxCount: undefined,
|
||||||
|
tip: 'Upload',
|
||||||
|
fit: 'contain',
|
||||||
|
draggable: true,
|
||||||
|
disabled: false,
|
||||||
|
spaceProps: () => ({}),
|
||||||
|
spinProps: () => ({}),
|
||||||
|
imageProps: () => ({}),
|
||||||
|
messageProps: () => ({}),
|
||||||
|
actionMessage: () => ({upload: '上传成功', remove: '删除成功'}),
|
||||||
|
beforeUpload: () => true,
|
||||||
|
uploadMode: 'base64',
|
||||||
|
customRequest: () => {
|
||||||
|
},
|
||||||
|
fileList: () => []
|
||||||
|
});
|
||||||
|
const uploadedFiles = ref<FileType[]>([]); // 上传文件列表
|
||||||
|
const showUpload = ref(1); // 展示的上传框
|
||||||
|
const uploading = ref<boolean[]>([]); // 上传中
|
||||||
|
const uploadInputRef = ref(); // 上传文件控件引用
|
||||||
|
const imageRef = ref();
|
||||||
|
const messageRef = ref();
|
||||||
|
const emits = defineEmits(['update:fileList', 'drop', 'change', 'preview', 'remove']);
|
||||||
|
const maxFileCount = computed(() => {
|
||||||
|
if (props.maxCount === undefined) {
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
return props.maxCount;
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
initUpload();
|
||||||
|
});
|
||||||
|
|
||||||
|
function initUpload() {
|
||||||
|
uploadedFiles.value = [...props.fileList];
|
||||||
|
if (uploadedFiles.value.length > maxFileCount.value) {
|
||||||
|
uploadedFiles.value.splice(maxFileCount.value);
|
||||||
|
}
|
||||||
|
if (props.disabled) {
|
||||||
|
// 禁用
|
||||||
|
showUpload.value = uploadedFiles.value.length;
|
||||||
|
} else {
|
||||||
|
if (uploadedFiles.value.length < maxFileCount.value) {
|
||||||
|
showUpload.value = props.fileList.length + 1;
|
||||||
|
} else {
|
||||||
|
showUpload.value = maxFileCount.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImage(url: string) {
|
||||||
|
// 检查 url 是否为图片
|
||||||
|
const imageUrlReg = /\.(jpg|jpeg|png|gif)$/i;
|
||||||
|
const base64Reg = /^data:image/;
|
||||||
|
return imageUrlReg.test(url) || base64Reg.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPDF(url: string) {
|
||||||
|
// 检查 url 是否为pdf
|
||||||
|
const pdfUrlReg = /\.pdf$/i;
|
||||||
|
const base64Reg = /^data:application\/pdf/;
|
||||||
|
return pdfUrlReg.test(url) || base64Reg.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(e: DragEvent, index: number) {
|
||||||
|
// 拖拽上传
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files?.length) {
|
||||||
|
const len = files.length;
|
||||||
|
for (let n = 0; n < len; n++) {
|
||||||
|
if (index + n <= maxFileCount.value) {
|
||||||
|
uploadFile(files[n], index + n);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// input的change事件默认保存上一次input的value值,同一value值(根据文件路径判断)在上传时不重新加载
|
||||||
|
uploadInputRef.value[index].value = '';
|
||||||
|
}
|
||||||
|
emits('drop', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickFileInput(index: number) {
|
||||||
|
uploadInputRef.value[index].click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpload(e: any, index: number) {
|
||||||
|
// 点击上传
|
||||||
|
const files = e.target.files;
|
||||||
|
if (files?.length) {
|
||||||
|
const len = files.length;
|
||||||
|
for (let n = 0; n < len; n++) {
|
||||||
|
if (index + n < maxFileCount.value) {
|
||||||
|
uploadFile(files[n], index + n);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// input的change事件默认保存上一次input的value值,同一value值(根据文件路径判断)在上传时不重新加载
|
||||||
|
uploadInputRef.value[index].value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFile = async (file: File, index: number) => {
|
||||||
|
// 统一上传文件方法
|
||||||
|
// console.log('开始上传 upload-event files:', file)
|
||||||
|
const promiseFunction = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// 尝试执行传入的函数,并获取返回值
|
||||||
|
const result = props.beforeUpload(file);
|
||||||
|
// 检查返回值是否是 Promise
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
// 如果是 Promise,则等待其 resolve 或 reject
|
||||||
|
result.then(resolve, reject);
|
||||||
|
} else {
|
||||||
|
// 检查返回值是否为布尔值
|
||||||
|
if (typeof result === 'boolean') {
|
||||||
|
// 如果是布尔值,根据值resolve或reject
|
||||||
|
if (result) {
|
||||||
|
resolve(result);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Function returned false'));
|
||||||
|
}
|
||||||
|
// result ? resolve(result) : reject(new Error('Function returned false'));
|
||||||
|
} else {
|
||||||
|
// 否则,直接resolve返回值
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 如果执行过程中抛出错误,则 reject 错误
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
promiseFunction()
|
||||||
|
.then(() => {
|
||||||
|
// 使用用户钩子进行上传前文件判断,例如大小、类型限制
|
||||||
|
if (maxFileCount.value > showUpload.value) {
|
||||||
|
showUpload.value++;
|
||||||
|
}
|
||||||
|
if (props.uploadMode === 'base64') {
|
||||||
|
// 以base64方式读取文件
|
||||||
|
uploading.value[index] = true;
|
||||||
|
base64Upload(file, index);
|
||||||
|
}
|
||||||
|
if (props.uploadMode === 'custom') {
|
||||||
|
// 自定义上传行为,需配合 customRequest 属性
|
||||||
|
uploading.value[index] = true;
|
||||||
|
customUpload(file, index);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log('beforeUpload error:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function base64Upload(file: File, index: number) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file); // 以base64方式读取文件
|
||||||
|
reader.onloadstart = function (_e) {
|
||||||
|
// 当读取操作开始时触发
|
||||||
|
// reader.abort() // 取消上传
|
||||||
|
// console.log('开始读取 onloadstart:', e)
|
||||||
|
};
|
||||||
|
reader.onabort = function (_e) {
|
||||||
|
// 当读取操作被中断时触发
|
||||||
|
// console.log('读取中止 onabort:', e)
|
||||||
|
};
|
||||||
|
reader.onerror = function (_e) {
|
||||||
|
// 当读取操作发生错误时触发
|
||||||
|
// console.log('读取错误 onerror:', e)
|
||||||
|
};
|
||||||
|
reader.onprogress = function (e) {
|
||||||
|
// 在读取Blob时触发,读取上传进度,50ms左右调用一次
|
||||||
|
// console.log('读取中 onprogress:', e)
|
||||||
|
// console.log('已读取:', Math.ceil(e.loaded / e.total * 100))
|
||||||
|
if (e.loaded === e.total) {
|
||||||
|
// 上传完成
|
||||||
|
uploading.value[index] = false; // 隐藏loading状态
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onload = function (e) {
|
||||||
|
// 当读取操作成功完成时调用
|
||||||
|
// console.log('读取成功 onload:', e)
|
||||||
|
// 该文件的base64数据,如果是图片,则前端可直接用来展示图片
|
||||||
|
uploadedFiles.value.push({
|
||||||
|
name: file.name,
|
||||||
|
url: e.target?.result
|
||||||
|
});
|
||||||
|
if (props.actionMessage.upload) {
|
||||||
|
messageRef.value.success(props.actionMessage.upload);
|
||||||
|
}
|
||||||
|
emits('update:fileList', uploadedFiles.value);
|
||||||
|
emits('change', uploadedFiles.value);
|
||||||
|
};
|
||||||
|
reader.onloadend = function (_e) {
|
||||||
|
// 当读取操作结束时触发(要么成功,要么失败)触发
|
||||||
|
// console.log('读取结束 onloadend:', e)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function customUpload(file: File, index: number) {
|
||||||
|
props
|
||||||
|
.customRequest(file)
|
||||||
|
.then((res: any) => {
|
||||||
|
uploadedFiles.value.push(res);
|
||||||
|
if (props.actionMessage.upload) {
|
||||||
|
messageRef.value.success(props.actionMessage.upload);
|
||||||
|
}
|
||||||
|
emits('update:fileList', uploadedFiles.value);
|
||||||
|
emits('change', uploadedFiles.value);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
if (maxFileCount.value > 1) {
|
||||||
|
showUpload.value = uploadedFiles.value.length + 1;
|
||||||
|
}
|
||||||
|
messageRef.value.error(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
uploading.value[index] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPreview(index: number, url: string) {
|
||||||
|
if (isImage(url)) {
|
||||||
|
const files = uploadedFiles.value.slice(0, index).filter((file) => !isImage(file.url));
|
||||||
|
imageRef.value[index - files.length].preview(0);
|
||||||
|
} else {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
emits('preview', uploadedFiles.value[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRemove(index: number) {
|
||||||
|
if (uploadedFiles.value.length < maxFileCount.value) {
|
||||||
|
showUpload.value--;
|
||||||
|
}
|
||||||
|
const removeFile = uploadedFiles.value.splice(index, 1);
|
||||||
|
if (props.actionMessage.remove) {
|
||||||
|
messageRef.value.success(props.actionMessage.remove);
|
||||||
|
}
|
||||||
|
emits('remove', removeFile[0]);
|
||||||
|
emits('update:fileList', uploadedFiles.value);
|
||||||
|
emits('change', uploadedFiles.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInfo(content: string) {
|
||||||
|
messageRef.value.info(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSuccess(content: string) {
|
||||||
|
messageRef.value.success(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(content: string) {
|
||||||
|
messageRef.value.error(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWarning(content: string) {
|
||||||
|
messageRef.value.warning(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoading(content: string) {
|
||||||
|
messageRef.value.loading(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
info: onInfo,
|
||||||
|
success: onSuccess,
|
||||||
|
error: onError,
|
||||||
|
warning: onWarning,
|
||||||
|
loading: onLoading
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-upload-wrap">
|
||||||
|
<Space gap="small" v-bind="spaceProps">
|
||||||
|
<div class="m-upload-item" v-for="n of showUpload" :key="n">
|
||||||
|
<div class="m-upload">
|
||||||
|
<div
|
||||||
|
v-show="!uploading[n - 1] && !uploadedFiles[n - 1]"
|
||||||
|
class="upload-item"
|
||||||
|
:class="{ 'upload-disabled': disabled }"
|
||||||
|
@dragenter.stop.prevent
|
||||||
|
@dragover.stop.prevent
|
||||||
|
@drop.stop.prevent="draggable && !disabled ? onDrop($event, n - 1) : () => false"
|
||||||
|
@click="!disabled ? onClickFileInput(n - 1) : () => false"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="uploadInputRef"
|
||||||
|
type="file"
|
||||||
|
@click.stop
|
||||||
|
:accept="accept"
|
||||||
|
:multiple="multiple"
|
||||||
|
@change="onUpload($event, n - 1)"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
focusable="false"
|
||||||
|
class="plus-svg"
|
||||||
|
data-icon="plus"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<defs></defs>
|
||||||
|
<path d="M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"></path>
|
||||||
|
<path d="M176 474h672q8 0 8 8v60q0 8-8 8H176q-8 0-8-8v-60q0-8 8-8z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="upload-tip">
|
||||||
|
<slot>{{ tip }}</slot>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="uploading[n - 1]" class="file-uploading">
|
||||||
|
<Spin class="spin-uploading" tip="uploading" size="small" indicator="spin-line" v-bind="spinProps"/>
|
||||||
|
</div>
|
||||||
|
<div v-if="uploadedFiles[n - 1]" class="file-preview">
|
||||||
|
<Image
|
||||||
|
v-if="isImage(uploadedFiles[n - 1].url)"
|
||||||
|
ref="imageRef"
|
||||||
|
:bordered="false"
|
||||||
|
:width="82"
|
||||||
|
:height="82"
|
||||||
|
:fit="fit"
|
||||||
|
:src="uploadedFiles[n - 1].url"
|
||||||
|
:name="uploadedFiles[n - 1].name"
|
||||||
|
v-bind="imageProps"
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
v-else-if="isPDF(uploadedFiles[n - 1].url)"
|
||||||
|
class="file-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="file-pdf"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M531.3 574.4l.3-1.4c5.8-23.9 13.1-53.7 7.4-80.7-3.8-21.3-19.5-29.6-32.9-30.2-15.8-.7-29.9 8.3-33.4 21.4-6.6 24-.7 56.8 10.1 98.6-13.6 32.4-35.3 79.5-51.2 107.5-29.6 15.3-69.3 38.9-75.2 68.7-1.2 5.5.2 12.5 3.5 18.8 3.7 7 9.6 12.4 16.5 15 3 1.1 6.6 2 10.8 2 17.6 0 46.1-14.2 84.1-79.4 5.8-1.9 11.8-3.9 17.6-5.9 27.2-9.2 55.4-18.8 80.9-23.1 28.2 15.1 60.3 24.8 82.1 24.8 21.6 0 30.1-12.8 33.3-20.5 5.6-13.5 2.9-30.5-6.2-39.6-13.2-13-45.3-16.4-95.3-10.2-24.6-15-40.7-35.4-52.4-65.8zM421.6 726.3c-13.9 20.2-24.4 30.3-30.1 34.7 6.7-12.3 19.8-25.3 30.1-34.7zm87.6-235.5c5.2 8.9 4.5 35.8.5 49.4-4.9-19.9-5.6-48.1-2.7-51.4.8.1 1.5.7 2.2 2zm-1.6 120.5c10.7 18.5 24.2 34.4 39.1 46.2-21.6 4.9-41.3 13-58.9 20.2-4.2 1.7-8.3 3.4-12.3 5 13.3-24.1 24.4-51.4 32.1-71.4zm155.6 65.5c.1.2.2.5-.4.9h-.2l-.2.3c-.8.5-9 5.3-44.3-8.6 40.6-1.9 45 7.3 45.1 7.4zm191.4-388.2L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="file-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="file"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div class="file-mask">
|
||||||
|
<a class="file-icon" title="预览" @click="onPreview(n - 1, uploadedFiles[n - 1].url)">
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="eye"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a v-show="!disabled" class="file-icon" title="删除" @click.prevent.stop="onRemove(n - 1)">
|
||||||
|
<svg
|
||||||
|
class="icon-svg"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="delete"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
<Message ref="messageRef" v-bind="messageProps"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-upload-wrap {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.m-upload-item {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr8 {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-upload {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
|
.upload-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.5714285714285714;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-uploading {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.spin-uploading {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
:deep(.spin-tip) {
|
||||||
|
max-width: 82px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.file-svg {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-mask {
|
||||||
|
// top right bottom left 简写为 inset: 0
|
||||||
|
// insert 无论元素的书写模式、行内方向和文本朝向如何,其所定义的都不是逻辑偏移而是实体偏移
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
display: inline-block;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.icon-svg {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
fill: currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.file-mask {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -344,4 +344,82 @@ export function useMutationObserver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消除 js 加减精度问题的加法函数
|
||||||
|
*
|
||||||
|
* 该函数旨在添加两个数字,考虑到它们可能是整数或小数;对于整数,直接返回它们的和
|
||||||
|
* 对于小数,为了确保精确计算,将小数转换为相同长度的字符串进行处理,然后将结果转换回小数
|
||||||
|
*
|
||||||
|
* @param num1 第一个数字
|
||||||
|
* @param num2 第二个数字
|
||||||
|
* @returns 返回两个数字的和
|
||||||
|
*/
|
||||||
|
export function add(num1: number, num2: number): number {
|
||||||
|
// 验证输入是否为有效的数字
|
||||||
|
// Number.isNaN() 不会尝试将参数转换为数字;全局 isNaN() 函数会将参数强制转换为数字
|
||||||
|
if (Number.isNaN(num1) || Number.isNaN(num2)) {
|
||||||
|
throw new Error('Both num1 and num2 must be valid numbers.');
|
||||||
|
}
|
||||||
|
// 检查输入是否为小数
|
||||||
|
const isDecimalNum1 = num1 % 1 !== 0;
|
||||||
|
const isDecimalNum2 = num2 % 1 !== 0;
|
||||||
|
if (!isDecimalNum1 && !isDecimalNum2) {
|
||||||
|
return num1 + num2; // 如果两个数字都是整数,则直接返回它们的和
|
||||||
|
}
|
||||||
|
const num1DeciStr = String(num1).split('.')[1] ?? '';
|
||||||
|
const num2DeciStr = String(num2).split('.')[1] ?? '';
|
||||||
|
const maxLen = Math.max(num1DeciStr.length, num2DeciStr.length);
|
||||||
|
const factor = Math.pow(10, maxLen);
|
||||||
|
const num1Str = num1.toFixed(maxLen);
|
||||||
|
const num2Str = num2.toFixed(maxLen);
|
||||||
|
// 将小数点移除并转换为整数相加
|
||||||
|
const result = (+num1Str.replace('.', '') + +num2Str.replace('.', '')) / factor;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数字格式化函数
|
||||||
|
*
|
||||||
|
* 该函数提供了一种灵活的方式将数字格式化为字符串,包括设置精度、千位分隔符、小数点字符、前缀和后缀
|
||||||
|
*
|
||||||
|
* @param value 要格式化的数字或数字字符串
|
||||||
|
* @param precision 小数点后的位数,默认为 2
|
||||||
|
* @param separator 千分位分隔符,默认为 ','
|
||||||
|
* @param decimal 小数点字符,默认为 '.'
|
||||||
|
* @param prefix 数字前的字符串,默认为 undefined
|
||||||
|
* @param suffix 数字后的字符串,默认为 undefined
|
||||||
|
* @returns 格式化后的字符串;如果输入值不是数字或字符串,则抛出类型错误
|
||||||
|
*/
|
||||||
|
export function formatNumber(
|
||||||
|
value: number | string,
|
||||||
|
precision: number = 2,
|
||||||
|
separator: string = ',',
|
||||||
|
decimal: string = '.',
|
||||||
|
prefix?: string,
|
||||||
|
suffix?: string
|
||||||
|
): string {
|
||||||
|
// 类型检查
|
||||||
|
if (typeof value !== 'number' && typeof value !== 'string') {
|
||||||
|
console.warn('Expected value to be of type number or string');
|
||||||
|
}
|
||||||
|
if (typeof precision !== 'number') {
|
||||||
|
console.warn('Expected precision to be of type number');
|
||||||
|
}
|
||||||
|
// 处理非数值或NaN的情况
|
||||||
|
const numValue = Number(value);
|
||||||
|
if (isNaN(numValue) || !isFinite(numValue)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (numValue === 0) {
|
||||||
|
return numValue.toFixed(precision);
|
||||||
|
}
|
||||||
|
let formatValue = numValue.toFixed(precision);
|
||||||
|
// 如果 separator 是数值而非字符串,会导致错误,此处进行检查
|
||||||
|
if (typeof separator === 'string' && separator !== '') {
|
||||||
|
const [integerPart, decimalPart] = formatValue.split('.');
|
||||||
|
formatValue =
|
||||||
|
integerPart.replace(/(\d)(?=(\d{3})+$)/g, '$1' + separator) + (decimalPart ? decimal + decimalPart : '');
|
||||||
|
}
|
||||||
|
return (prefix || '') + formatValue + (suffix || '');
|
||||||
|
}
|
||||||
217
src/components/MyUI/Video/Video.vue
Normal file
217
src/components/MyUI/Video/Video.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, onMounted, ref} from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
src?: string // 视频文件地址,支持网络地址 https 和相对地址
|
||||||
|
poster?: string // 视频封面地址,支持网络地址 https 和相对地址
|
||||||
|
second?: number // 在未设置封面时,自动截取视频第 second 秒对应帧作为视频封面,单位 s
|
||||||
|
width?: string | number // 视频播放器宽度,单位 px
|
||||||
|
height?: string | number // 视频播放器高度,单位 px
|
||||||
|
autoplay?: boolean // 视频就绪后是否马上播放,优先级高于 preload
|
||||||
|
controls?: boolean // 是否向用户显示控件,比如进度条,全屏等
|
||||||
|
loop?: boolean // 视频播放完成后,是否循环播放
|
||||||
|
muted?: boolean // 是否静音
|
||||||
|
preload?: 'auto' | 'metadata' | 'none' // 是否在页面加载后载入视频,如果设置了 autoplay 属性,则 preload 将被忽略
|
||||||
|
showPlay?: boolean // 播放暂停时是否显示播放器中间的暂停图标
|
||||||
|
fit?: 'none' | 'fill' | 'contain' | 'cover' // video 的 poster 默认图片和视频内容缩放规则
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
src: undefined,
|
||||||
|
poster: undefined,
|
||||||
|
second: 0.5,
|
||||||
|
width: 800,
|
||||||
|
height: 450,
|
||||||
|
/*
|
||||||
|
参考 MDN 自动播放指南:https://developer.mozilla.org/zh-CN/docs/Web/Media/Autoplay_guide
|
||||||
|
Autoplay 功能
|
||||||
|
据新政策,媒体内容将在满足以下至少一个的条件下自动播放:
|
||||||
|
1.音频被静音或其音量设置为 0
|
||||||
|
2.用户和网页已有交互行为(包括点击、触摸、按下某个键等等)
|
||||||
|
3.网站已被列入白名单;如果浏览器确定用户经常与媒体互动,这可能会自动发生,也可能通过首选项或其他用户界面功能手动发生
|
||||||
|
4.自动播放策略应用到<iframe>或者其文档上
|
||||||
|
autoplay:由于目前在最新版的Chrome浏览器(以及所有以Chromium为内核的浏览器)中,
|
||||||
|
已不再允许自动播放音频和视频。就算你为video或audio标签设置了autoplay属性也一样不能自动播放!
|
||||||
|
解决方法:设置视频 autoplay 时,视频必须设置为静音 muted: true 即可实现自动播放,
|
||||||
|
然后用户可以使用控制栏开启声音,类似某宝商品自动播放的宣传视频逻辑
|
||||||
|
*/
|
||||||
|
autoplay: false,
|
||||||
|
controls: true,
|
||||||
|
loop: false,
|
||||||
|
muted: false,
|
||||||
|
/*
|
||||||
|
preload可选属性:
|
||||||
|
auto: 一旦页面加载,则开始加载视频;
|
||||||
|
metadata: 当页面加载后仅加载视频的元数据(例如长度),建议使用 metadata,以便视频自动获取第一帧作为封面 poster
|
||||||
|
none: 页面加载后不应加载视频
|
||||||
|
*/
|
||||||
|
preload: 'metadata',
|
||||||
|
showPlay: true,
|
||||||
|
/*
|
||||||
|
fit可选属性:
|
||||||
|
none: 保存原有内容,不进行缩放;
|
||||||
|
fill: 不保持原有比例,内容拉伸填充整个内容容器;
|
||||||
|
contain: 保存原有比例,内容以包含方式缩放;
|
||||||
|
cover: 保存原有比例,内容以覆盖方式缩放
|
||||||
|
*/
|
||||||
|
fit: 'contain'
|
||||||
|
});
|
||||||
|
const veoRef = ref();
|
||||||
|
// 参考文档:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/video
|
||||||
|
const veoPoster = ref();
|
||||||
|
const originPlay = ref(true);
|
||||||
|
const hidden = ref(false); // 是否隐藏播放器中间的播放按钮
|
||||||
|
const emits = defineEmits(['play', 'pause']);
|
||||||
|
const veoWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`;
|
||||||
|
}
|
||||||
|
return props.width;
|
||||||
|
});
|
||||||
|
const veoHeight = computed(() => {
|
||||||
|
if (typeof props.height === 'number') {
|
||||||
|
return `${props.height}px`;
|
||||||
|
}
|
||||||
|
return props.height;
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.autoplay) {
|
||||||
|
hidden.value = true;
|
||||||
|
originPlay.value = false;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
自定义设置播放速度,经测试:
|
||||||
|
在vue2中需设置:this.$refs.veoRef.playbackRate = 2
|
||||||
|
在vue3中需设置:veoRef.value.defaultPlaybackRate = 2
|
||||||
|
*/
|
||||||
|
// veoRef.value.defaultPlaybackRate = 2
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
loadedmetadata 事件在元数据(metadata)被加载完成后触发
|
||||||
|
loadeddata 事件在媒体当前播放位置的视频帧(通常是第一帧)加载完成后触发
|
||||||
|
若在移动/平板设备的浏览器设置中开启了流量节省(data-saver)模式,该事件则不会被触发。
|
||||||
|
preload 为 none 时不会触发
|
||||||
|
*/
|
||||||
|
function getPoster() {
|
||||||
|
// 在未设置封面时,自动截取视频0.5s对应帧作为视频封面
|
||||||
|
// 由于不少视频第一帧为黑屏,故设置视频开始播放时间为0.5s,即取该时刻帧作为封面图
|
||||||
|
veoRef.value.currentTime = props.second;
|
||||||
|
// 创建canvas元素
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
// canvas画图
|
||||||
|
canvas.width = veoRef.value.videoWidth;
|
||||||
|
canvas.height = veoRef.value.videoHeight;
|
||||||
|
ctx?.drawImage(veoRef.value, 0, 0, canvas.width, canvas.height);
|
||||||
|
// 把canvas转成base64编码格式
|
||||||
|
veoPoster.value = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlay() {
|
||||||
|
if (originPlay.value) {
|
||||||
|
veoRef.value.currentTime = 0;
|
||||||
|
originPlay.value = false;
|
||||||
|
}
|
||||||
|
if (props.autoplay) {
|
||||||
|
veoRef.value?.pause();
|
||||||
|
emits('pause');
|
||||||
|
} else {
|
||||||
|
hidden.value = true;
|
||||||
|
veoRef.value?.play();
|
||||||
|
emits('play');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPause() {
|
||||||
|
hidden.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlaying() {
|
||||||
|
hidden.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="m-video" :class="{ 'video-hover': !hidden }" :style="`width: ${veoWidth}; height: ${veoHeight};`">
|
||||||
|
<video
|
||||||
|
ref="veoRef"
|
||||||
|
class="u-video"
|
||||||
|
:style="`object-fit: ${fit};`"
|
||||||
|
:src="src"
|
||||||
|
:poster="poster ? poster : veoPoster"
|
||||||
|
:autoplay="autoplay"
|
||||||
|
:controls="!originPlay && controls"
|
||||||
|
:loop="loop"
|
||||||
|
:muted="autoplay || muted"
|
||||||
|
:preload="preload"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
@loadedmetadata="poster ? () => false : getPoster()"
|
||||||
|
@pause="showPlay ? onPause() : () => false"
|
||||||
|
@playing="showPlay ? onPlaying() : () => false"
|
||||||
|
@click.prevent.once="onPlay"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
您的浏览器不支持video标签。
|
||||||
|
</video>
|
||||||
|
<span v-show="originPlay || showPlay" class="icon-play" :class="{ 'icon-hidden': hidden }">
|
||||||
|
<svg class="play-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 34">
|
||||||
|
<path
|
||||||
|
d="M28.26,11.961L11.035,0.813C7.464-1.498,3,1.391,3,6.013v21.974c0,4.622,4.464,7.511,8.035,5.2L28.26,22.039
|
||||||
|
C31.913,19.675,31.913,14.325,28.26,11.961z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-video {
|
||||||
|
position: relative;
|
||||||
|
background: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.u-video {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-play {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
|
||||||
|
.play-svg {
|
||||||
|
display: inline-block;
|
||||||
|
color: #fff;
|
||||||
|
fill: currentColor;
|
||||||
|
width: 29px;
|
||||||
|
height: 34px;
|
||||||
|
margin-top: 23px;
|
||||||
|
margin-left: 27px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-hover {
|
||||||
|
&:hover {
|
||||||
|
.icon-play {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import Spin from '../Spin/Spin.vue';
|
import Spin from '../Spin/Spin.vue';
|
||||||
import { useResizeObserver } from '../utils';
|
import { useResizeObserver } from '../Utils';
|
||||||
/*
|
/*
|
||||||
宽度固定,图片等比例缩放;使用JS获取每张图片宽度和高度,结合 `relative` 和 `absolute` 定位
|
宽度固定,图片等比例缩放;使用JS获取每张图片宽度和高度,结合 `relative` 和 `absolute` 定位
|
||||||
计算每个图片的位置 `top`,`left`,保证每张新的图片都追加在当前高度最小的那列末尾
|
计算每个图片的位置 `top`,`left`,保证每张新的图片都追加在当前高度最小的那列末尾
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="header-main">
|
<header class="header-main">
|
||||||
<AFlex :vertical="false" align="center" justify="flex-end" class="header-logo-container">
|
<div class="header-container">
|
||||||
<AAvatar :size="50" :src="logo"/>
|
<AFlex :vertical="false" align="center" justify="flex-end" class="header-logo-container">
|
||||||
<img style="width: 200px;" src="@/assets/images/logo.png" alt="logo">
|
<AAvatar :size="50" :src="logo"/>
|
||||||
</AFlex>
|
<img style="width: 200px;" src="@/assets/images/logo.png" alt="logo">
|
||||||
<AFlex :vertical="false" align="center" justify="flex-end" class="header-menu-container">
|
</AFlex>
|
||||||
<AFlex :vertical="false" align="center" justify="flex-start" class="header-menu-item">
|
<AFlex :vertical="false" align="center" justify="flex-end" class="header-menu-container">
|
||||||
<ABadge count="0" :numberStyle="{
|
<AFlex :vertical="false" align="center" justify="flex-start" class="header-menu-item">
|
||||||
|
<ABadge count="0" :numberStyle="{
|
||||||
marginTop: '5px',
|
marginTop: '5px',
|
||||||
}">
|
}">
|
||||||
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn"
|
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn"
|
||||||
:icon="h(BellOutlined)"/>
|
:icon="h(BellOutlined)"/>
|
||||||
</ABadge>
|
</ABadge>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center" justify="flex-start" class="header-menu-item">
|
||||||
|
<ADropdown>
|
||||||
|
<template #overlay>
|
||||||
|
<AMenu @click="changeLang">
|
||||||
|
<AMenuItem key="zh">{{ t("landing.chinese") }}</AMenuItem>
|
||||||
|
<AMenuItem key="en">{{ t("landing.english") }}</AMenuItem>
|
||||||
|
</AMenu>
|
||||||
|
</template>
|
||||||
|
<AButton type="text" shape="circle" size="large" :icon="h(TranslationOutlined)">
|
||||||
|
</AButton>
|
||||||
|
</ADropdown>
|
||||||
|
</AFlex>
|
||||||
|
<AFlex :vertical="false" align="center" justify="flex-start" class="header-user-container">
|
||||||
|
<AAvatar :size="35" class="header-user-avatar" :src="user.user.avatar"/>
|
||||||
|
</AFlex>
|
||||||
</AFlex>
|
</AFlex>
|
||||||
<AFlex :vertical="false" align="center" justify="flex-start" class="header-menu-item">
|
</div>
|
||||||
<ADropdown>
|
|
||||||
<template #overlay>
|
|
||||||
<AMenu @click="changeLang">
|
|
||||||
<AMenuItem key="zh">{{ t("landing.chinese") }}</AMenuItem>
|
|
||||||
<AMenuItem key="en">{{ t("landing.english") }}</AMenuItem>
|
|
||||||
</AMenu>
|
|
||||||
</template>
|
|
||||||
<AButton type="text" shape="circle" size="large" :icon="h(TranslationOutlined)">
|
|
||||||
</AButton>
|
|
||||||
</ADropdown>
|
|
||||||
</AFlex>
|
|
||||||
<AFlex :vertical="false" align="center" justify="flex-start" class="header-user-container">
|
|
||||||
<AAvatar :size="35" class="header-user-avatar" :src="user.user.avatar"/>
|
|
||||||
<AButton type="text" size="small" class="header-user-btn">landaiqing</AButton>
|
|
||||||
</AFlex>
|
|
||||||
</AFlex>
|
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
.header-main {
|
.header-main {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1200px;
|
min-height: 70px;
|
||||||
min-height: 60px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
background-color: rgba(255, 255, 255, 0.38);
|
background-color: rgba(255, 255, 255, 0.38);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||||
//border-radius: 20px;
|
//border-radius: 20px;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 1200px;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-logo-container {
|
.header-logo-container {
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
|
|||||||
@@ -8,23 +8,13 @@ const messages = {
|
|||||||
zh
|
zh
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const language: string = (navigator.language || 'en').toLocaleLowerCase(); // 获取浏览器的语言
|
const language: string = (navigator.language || 'en').toLocaleLowerCase(); // 获取浏览器的语言
|
||||||
function getLanguage(): string | null {
|
|
||||||
let lang: string | null = null;
|
|
||||||
const langStr: string | null = localStorage.getItem('lang');
|
|
||||||
if (langStr) {
|
|
||||||
lang = JSON.parse(langStr).lang;
|
|
||||||
}
|
|
||||||
return lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
const i18n: any = createI18n({
|
const i18n: any = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
compositionOnly: false,
|
compositionOnly: false,
|
||||||
globalInjection: true,
|
globalInjection: true,
|
||||||
silentTranslationWarn: true,
|
silentTranslationWarn: true,
|
||||||
locale: getLanguage() || language.split('-')[0] || 'zh', // 首先从缓存里拿,没有的话就用浏览器语言,
|
locale: language.split('-')[0] || 'zh',
|
||||||
silentFallbackWarn: true,
|
silentFallbackWarn: true,
|
||||||
missingWarn: true,
|
missingWarn: true,
|
||||||
fallbackWarn: false,
|
fallbackWarn: false,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-page">
|
<div class="main-page">
|
||||||
<Header/>
|
<Header/>
|
||||||
<CommentReply/>
|
|
||||||
|
<div style="margin-top: 100px">
|
||||||
|
<CommentReply/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
176
yarn.lock
176
yarn.lock
@@ -1135,7 +1135,14 @@
|
|||||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz#f9e45b7f63f2c3f40d84237b1194b7f67de192e3"
|
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz#f9e45b7f63f2c3f40d84237b1194b7f67de192e3"
|
||||||
integrity sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==
|
integrity sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==
|
||||||
|
|
||||||
"@vueuse/core@^11.2.0":
|
"@vuepic/vue-datepicker@^10.0.0":
|
||||||
|
version "10.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@vuepic/vue-datepicker/-/vue-datepicker-10.0.0.tgz#46b88a7b82a76ce6e2576581cc086543161a6e62"
|
||||||
|
integrity sha512-ujlk3ahftVQpyCJ8hq7TmOOHrf/XFJI1ZcAh/FRB5Ci62Vq5HmHf6xux5KVi5SPUFRTJY78m+uDhYy1M+8RZ9w==
|
||||||
|
dependencies:
|
||||||
|
date-fns "^4.1.0"
|
||||||
|
|
||||||
|
"@vueuse/core@11.2.0", "@vueuse/core@^11.2.0":
|
||||||
version "11.2.0"
|
version "11.2.0"
|
||||||
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-11.2.0.tgz#3fc6c0963051bb154dc4c08061889405e3fc745d"
|
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-11.2.0.tgz#3fc6c0963051bb154dc4c08061889405e3fc745d"
|
||||||
integrity sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==
|
integrity sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==
|
||||||
@@ -1145,6 +1152,15 @@
|
|||||||
"@vueuse/shared" "11.2.0"
|
"@vueuse/shared" "11.2.0"
|
||||||
vue-demi ">=0.14.10"
|
vue-demi ">=0.14.10"
|
||||||
|
|
||||||
|
"@vueuse/integrations@^11.2.0":
|
||||||
|
version "11.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@vueuse/integrations/-/integrations-11.2.0.tgz#c4c2dd82260265697e0ec8d6356a7d2744acb896"
|
||||||
|
integrity sha512-zGXz3dsxNHKwiD9jPMvR3DAxQEOV6VWIEYTGVSB9PNpk4pTWR+pXrHz9gvXWcP2sTk3W2oqqS6KwWDdntUvNVA==
|
||||||
|
dependencies:
|
||||||
|
"@vueuse/core" "11.2.0"
|
||||||
|
"@vueuse/shared" "11.2.0"
|
||||||
|
vue-demi ">=0.14.10"
|
||||||
|
|
||||||
"@vueuse/metadata@11.2.0":
|
"@vueuse/metadata@11.2.0":
|
||||||
version "11.2.0"
|
version "11.2.0"
|
||||||
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-11.2.0.tgz#fd02cbbc7d08cb4592fceea0486559b89ae38643"
|
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-11.2.0.tgz#fd02cbbc7d08cb4592fceea0486559b89ae38643"
|
||||||
@@ -1570,6 +1586,11 @@ camel-case@^4.1.2:
|
|||||||
pascal-case "^3.1.2"
|
pascal-case "^3.1.2"
|
||||||
tslib "^2.0.3"
|
tslib "^2.0.3"
|
||||||
|
|
||||||
|
camelcase@^5.0.0:
|
||||||
|
version "5.3.1"
|
||||||
|
resolved "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||||
|
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001663:
|
caniuse-lite@^1.0.30001663:
|
||||||
version "1.0.30001669"
|
version "1.0.30001669"
|
||||||
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3"
|
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3"
|
||||||
@@ -1665,6 +1686,15 @@ clean-css@^5.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
source-map "~0.6.0"
|
source-map "~0.6.0"
|
||||||
|
|
||||||
|
cliui@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
|
||||||
|
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
|
||||||
|
dependencies:
|
||||||
|
string-width "^4.2.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
wrap-ansi "^6.2.0"
|
||||||
|
|
||||||
cliui@^7.0.2:
|
cliui@^7.0.2:
|
||||||
version "7.0.4"
|
version "7.0.4"
|
||||||
resolved "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
resolved "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||||
@@ -1895,6 +1925,11 @@ csstype@^3.1.1, csstype@^3.1.3:
|
|||||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||||
|
|
||||||
|
date-fns@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14"
|
||||||
|
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
|
||||||
|
|
||||||
dayjs@^1.10.5:
|
dayjs@^1.10.5:
|
||||||
version "1.11.13"
|
version "1.11.13"
|
||||||
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
||||||
@@ -1912,6 +1947,11 @@ debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.3"
|
ms "^2.1.3"
|
||||||
|
|
||||||
|
decamelize@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
|
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||||
|
|
||||||
deep-is@^0.1.3, deep-is@~0.1.3:
|
deep-is@^0.1.3, deep-is@~0.1.3:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||||
@@ -1977,6 +2017,11 @@ diffie-hellman@^5.0.0:
|
|||||||
miller-rabin "^4.0.0"
|
miller-rabin "^4.0.0"
|
||||||
randombytes "^2.0.0"
|
randombytes "^2.0.0"
|
||||||
|
|
||||||
|
dijkstrajs@^1.0.1:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23"
|
||||||
|
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
|
||||||
|
|
||||||
dom-align@^1.12.1:
|
dom-align@^1.12.1:
|
||||||
version "1.12.4"
|
version "1.12.4"
|
||||||
resolved "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz#3503992eb2a7cfcb2ed3b2a6d21e0b9c00d54511"
|
resolved "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz#3503992eb2a7cfcb2ed3b2a6d21e0b9c00d54511"
|
||||||
@@ -2045,6 +2090,14 @@ dotenv@^16.0.0, dotenv@^16.4.5:
|
|||||||
resolved "https://registry.npmmirror.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
resolved "https://registry.npmmirror.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
||||||
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
||||||
|
|
||||||
|
echarts@^5.5.1:
|
||||||
|
version "5.5.1"
|
||||||
|
resolved "https://registry.npmmirror.com/echarts/-/echarts-5.5.1.tgz#8dc9c68d0c548934bedcb5f633db07ed1dd2101c"
|
||||||
|
integrity sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==
|
||||||
|
dependencies:
|
||||||
|
tslib "2.3.0"
|
||||||
|
zrender "5.6.0"
|
||||||
|
|
||||||
ejs@^3.1.6:
|
ejs@^3.1.6:
|
||||||
version "3.1.10"
|
version "3.1.10"
|
||||||
resolved "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
|
resolved "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
|
||||||
@@ -2398,6 +2451,14 @@ fill-range@^7.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
|
find-up@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
|
||||||
|
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
||||||
|
dependencies:
|
||||||
|
locate-path "^5.0.0"
|
||||||
|
path-exists "^4.0.0"
|
||||||
|
|
||||||
find-up@^5.0.0:
|
find-up@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
|
resolved "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
|
||||||
@@ -2471,7 +2532,7 @@ gensync@^1.0.0-beta.2:
|
|||||||
resolved "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
resolved "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||||
|
|
||||||
get-caller-file@^2.0.5:
|
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||||
@@ -3044,6 +3105,13 @@ localforage@^1.10.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lie "3.1.1"
|
lie "3.1.1"
|
||||||
|
|
||||||
|
locate-path@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
|
||||||
|
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
|
||||||
|
dependencies:
|
||||||
|
p-locate "^4.1.0"
|
||||||
|
|
||||||
locate-path@^6.0.0:
|
locate-path@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
|
resolved "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
|
||||||
@@ -3479,6 +3547,13 @@ os-browserify@^0.3.0:
|
|||||||
resolved "https://registry.npmmirror.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
resolved "https://registry.npmmirror.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
||||||
integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==
|
integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==
|
||||||
|
|
||||||
|
p-limit@^2.2.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||||
|
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
|
||||||
|
dependencies:
|
||||||
|
p-try "^2.0.0"
|
||||||
|
|
||||||
p-limit@^3.0.2:
|
p-limit@^3.0.2:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
|
resolved "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
|
||||||
@@ -3486,6 +3561,13 @@ p-limit@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yocto-queue "^0.1.0"
|
yocto-queue "^0.1.0"
|
||||||
|
|
||||||
|
p-locate@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
|
||||||
|
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
|
||||||
|
dependencies:
|
||||||
|
p-limit "^2.2.0"
|
||||||
|
|
||||||
p-locate@^5.0.0:
|
p-locate@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
|
resolved "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
|
||||||
@@ -3493,6 +3575,11 @@ p-locate@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit "^3.0.2"
|
p-limit "^3.0.2"
|
||||||
|
|
||||||
|
p-try@^2.0.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||||
|
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||||
|
|
||||||
pako@~1.0.5:
|
pako@~1.0.5:
|
||||||
version "1.0.11"
|
version "1.0.11"
|
||||||
resolved "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
resolved "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
@@ -3643,6 +3730,11 @@ pkg-types@^1.0.3, pkg-types@^1.2.0:
|
|||||||
mlly "^1.7.2"
|
mlly "^1.7.2"
|
||||||
pathe "^1.1.2"
|
pathe "^1.1.2"
|
||||||
|
|
||||||
|
pngjs@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||||
|
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||||
|
|
||||||
possible-typed-array-names@^1.0.0:
|
possible-typed-array-names@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
|
resolved "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
|
||||||
@@ -3717,6 +3809,15 @@ punycode@^2.1.0:
|
|||||||
resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||||
|
|
||||||
|
qrcode@^1:
|
||||||
|
version "1.5.4"
|
||||||
|
resolved "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88"
|
||||||
|
integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs "^1.0.1"
|
||||||
|
pngjs "^5.0.0"
|
||||||
|
yargs "^15.3.1"
|
||||||
|
|
||||||
qs@^6.12.3:
|
qs@^6.12.3:
|
||||||
version "6.13.0"
|
version "6.13.0"
|
||||||
resolved "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
|
resolved "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
|
||||||
@@ -3821,6 +3922,11 @@ require-directory@^2.1.1:
|
|||||||
resolved "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
resolved "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||||
|
|
||||||
|
require-main-filename@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||||
|
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||||
|
|
||||||
resize-observer-polyfill@^1.5.1:
|
resize-observer-polyfill@^1.5.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||||
@@ -3947,6 +4053,11 @@ semver@^7.3.6, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3:
|
|||||||
resolved "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
|
resolved "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
|
||||||
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
|
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
|
||||||
|
|
||||||
|
set-blocking@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
|
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
||||||
|
|
||||||
set-function-length@^1.2.1:
|
set-function-length@^1.2.1:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
|
resolved "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
|
||||||
@@ -4138,6 +4249,11 @@ supports-preserve-symlinks-flag@^1.0.0:
|
|||||||
resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||||
|
|
||||||
|
swiper@^11.1.14:
|
||||||
|
version "11.1.14"
|
||||||
|
resolved "https://registry.npmmirror.com/swiper/-/swiper-11.1.14.tgz#7901b4955c46dd0ad76fac1e9de2a69f04f34abb"
|
||||||
|
integrity sha512-VbQLQXC04io6AoAjIUWuZwW4MSYozkcP9KjLdrsG/00Q/yiwvhz9RQyt0nHXV10hi9NVnDNy1/wv7Dzq1lkOCQ==
|
||||||
|
|
||||||
tar@^6.2.0:
|
tar@^6.2.0:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
|
resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
|
||||||
@@ -4199,6 +4315,11 @@ ts-api-utils@^1.3.0:
|
|||||||
resolved "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
|
resolved "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
|
||||||
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
||||||
|
|
||||||
|
tslib@2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||||
|
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||||
|
|
||||||
tslib@2.5.0:
|
tslib@2.5.0:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
||||||
@@ -4566,6 +4687,11 @@ whatwg-url@^5.0.0:
|
|||||||
tr46 "~0.0.3"
|
tr46 "~0.0.3"
|
||||||
webidl-conversions "^3.0.0"
|
webidl-conversions "^3.0.0"
|
||||||
|
|
||||||
|
which-module@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
|
||||||
|
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
|
||||||
|
|
||||||
which-typed-array@^1.1.14, which-typed-array@^1.1.2:
|
which-typed-array@^1.1.14, which-typed-array@^1.1.2:
|
||||||
version "1.1.15"
|
version "1.1.15"
|
||||||
resolved "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d"
|
resolved "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d"
|
||||||
@@ -4589,6 +4715,15 @@ word-wrap@^1.2.5, word-wrap@~1.2.3:
|
|||||||
resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||||
|
|
||||||
|
wrap-ansi@^6.2.0:
|
||||||
|
version "6.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||||
|
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
@@ -4613,6 +4748,11 @@ xtend@^4.0.2:
|
|||||||
resolved "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||||
|
|
||||||
|
y18n@^4.0.0:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||||
|
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
@@ -4628,11 +4768,36 @@ yallist@^4.0.0:
|
|||||||
resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||||
|
|
||||||
|
yargs-parser@^18.1.2:
|
||||||
|
version "18.1.3"
|
||||||
|
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
|
||||||
|
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
||||||
|
dependencies:
|
||||||
|
camelcase "^5.0.0"
|
||||||
|
decamelize "^1.2.0"
|
||||||
|
|
||||||
yargs-parser@^20.2.2:
|
yargs-parser@^20.2.2:
|
||||||
version "20.2.9"
|
version "20.2.9"
|
||||||
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||||
|
|
||||||
|
yargs@^15.3.1:
|
||||||
|
version "15.4.1"
|
||||||
|
resolved "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||||
|
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||||
|
dependencies:
|
||||||
|
cliui "^6.0.0"
|
||||||
|
decamelize "^1.2.0"
|
||||||
|
find-up "^4.1.0"
|
||||||
|
get-caller-file "^2.0.1"
|
||||||
|
require-directory "^2.1.1"
|
||||||
|
require-main-filename "^2.0.0"
|
||||||
|
set-blocking "^2.0.0"
|
||||||
|
string-width "^4.2.0"
|
||||||
|
which-module "^2.0.0"
|
||||||
|
y18n "^4.0.0"
|
||||||
|
yargs-parser "^18.1.2"
|
||||||
|
|
||||||
yargs@^16.0.3:
|
yargs@^16.0.3:
|
||||||
version "16.2.0"
|
version "16.2.0"
|
||||||
resolved "https://registry.npmmirror.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
resolved "https://registry.npmmirror.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||||
@@ -4650,3 +4815,10 @@ yocto-queue@^0.1.0:
|
|||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zrender@5.6.0:
|
||||||
|
version "5.6.0"
|
||||||
|
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.6.0.tgz#01325b0bb38332dd5e87a8dbee7336cafc0f4a5b"
|
||||||
|
integrity sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==
|
||||||
|
dependencies:
|
||||||
|
tslib "2.3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user