add comment user info card

This commit is contained in:
landaiqing
2024-11-05 02:11:20 +08:00
parent ec55a2f8c8
commit c0269dfa5a
69 changed files with 2958 additions and 819 deletions

18
components.d.ts vendored
View File

@@ -36,15 +36,12 @@ declare module 'vue' {
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStep: typeof import('ant-design-vue/es')['Step']
ASteps: typeof import('ant-design-vue/es')['Steps']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
Badge: typeof import('./src/components/MyUI/Badge/Badge.vue')['default']
BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default']
Card3D: typeof import('./src/components/Card3D/Card3D.vue')['default']
@@ -53,12 +50,15 @@ declare module 'vue' {
CommentInput: typeof import('./src/components/CommentReply/src/CommentInput/CommentInput.vue')['default']
CommentList: typeof import('./src/components/CommentReply/src/CommentList/CommentList.vue')['default']
CommentReply: typeof import('./src/components/CommentReply/index.vue')['default']
DivTextarea: typeof import('./src/components/MyUI/Textarea/DivTextarea.vue')['default']
DownloadOutlined: typeof import('@ant-design/icons-vue')['DownloadOutlined']
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
EffectsCard: typeof import('./src/components/EffectsCard/EffectsCard.vue')['default']
Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.vue')['default']
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
GithubOutlined: typeof import('@ant-design/icons-vue')['GithubOutlined']
InboxOutlined: typeof import('@ant-design/icons-vue')['InboxOutlined']
GradientText: typeof import('./src/components/MyUI/GradientText/GradientText.vue')['default']
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
@@ -66,6 +66,8 @@ declare module 'vue' {
MainPage: typeof import('./src/views/Main/MainPage.vue')['default']
MessageReport: typeof import('./src/components/CommentReply/src/MessageReport/MessageReport.vue')['default']
NotFound: typeof import('./src/views/404/NotFound.vue')['default']
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
QqOutlined: typeof import('@ant-design/icons-vue')['QqOutlined']
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default']
@@ -75,10 +77,16 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SafetyOutlined: typeof import('@ant-design/icons-vue')['SafetyOutlined']
Scrollbar: typeof import('./src/components/MyUI/Scrollbar/Scrollbar.vue')['default']
SendOutlined: typeof import('@ant-design/icons-vue')['SendOutlined']
Spin: typeof import('./src/components/MyUI/Spin/Spin.vue')['default']
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
TypeSelect: typeof import('./src/components/CommentReply/src/MessageReport/TypeSelect.vue')['default']
Textarea: typeof import('./src/components/MyUI/Textarea/Textarea.vue')['default']
Tooltip: typeof import('./src/components/MyUI/Tooltip/Tooltip.vue')['default']
UserInfoCard: typeof import('./src/components/CommentReply/src/UserInfoCard/UserInfoCard.vue')['default']
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
Waterfall: typeof import('./src/components/MyUI/Waterfall/Waterfall.vue')['default']
WechatOutlined: typeof import('@ant-design/icons-vue')['WechatOutlined']
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 109.3 42.45">
<defs>
<style>
.cls-1 {
fill: url(#_未命名的渐变_3);
}
.cls-1, .cls-2, .cls-3, .cls-4 {
stroke-linejoin: round;
}
.cls-1, .cls-5, .cls-4 {
stroke: #fff;
}
.cls-6 {
font-size: 18px;
}
.cls-6, .cls-2, .cls-3, .cls-4 {
stroke-width: 2px;
}
.cls-7 {
filter: url(#drop-shadow-1);
}
.cls-2 {
fill: url(#_未命名的渐变_15);
}
.cls-2, .cls-3 {
stroke: #333;
}
.cls-3 {
fill: url(#_未命名的渐变_17);
}
.cls-8 {
font-size: 24px;
}
.cls-5 {
fill: #fff;
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke-miterlimit: 10;
}
.cls-4 {
fill: none;
stroke-linecap: round;
}
</style>
<linearGradient id="_未命名的渐变_15" data-name="未命名的渐变 15" x1="85.37" y1="-4.52" x2="26.64" y2="54.21" gradientUnits="userSpaceOnUse">
<stop offset=".04" stop-color="#ff931e"/>
<stop offset=".56" stop-color="#ff931e"/>
</linearGradient>
<filter id="drop-shadow-1" filterUnits="userSpaceOnUse">
<feOffset dx="5" dy="7"/>
<feGaussianBlur result="blur" stdDeviation="9"/>
<feFlood flood-color="#000" flood-opacity=".43"/>
<feComposite in2="blur" operator="in"/>
<feComposite in="SourceGraphic"/>
</filter>
<linearGradient id="_未命名的渐变_17" data-name="未命名的渐变 17" x1="16.17" y1="41.45" x2="16.17" y2="1" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ff931e"/>
<stop offset=".76" stop-color="#ffb855"/>
</linearGradient>
<linearGradient id="_未命名的渐变_3" data-name="未命名的渐变 3" x1="8.3" y1="31.5" x2="8.3" y2="30.5" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="red"/>
<stop offset=".7" stop-color="#93278f"/>
</linearGradient>
</defs>
<rect class="cls-2" x="3.71" y="9.31" width="104.58" height="31.08" rx="15.54" ry="15.54"/>
<g class="cls-7">
<path class="cls-3" d="M16.17,41.45c8.33,0,15.17-6.6,15.17-15.07,0-2.08-.11-4.31-1.26-7.77-1.15-3.46-1.38-3.91-2.6-6.05-.52,4.37-3.31,6.19-4.01,6.73,0-.57-1.69-6.82-4.24-10.57-2.51-3.67-5.92-6.09-7.92-7.72,0,3.1-.87,7.72-2.12,10.07s-1.49,2.44-3.05,4.19c-1.56,1.75-2.28,2.29-3.58,4.41-1.31,2.12-1.54,4.95-1.54,7.03,0,8.47,6.84,14.75,15.17,14.75Z"/>
</g>
<path class="cls-4" d="M6.3,22c-.16,.78-.27,1.79-.15,2.96,.13,1.21,.46,2.2,.79,2.92"/>
<circle class="cls-1" cx="8.3" cy="31" r=".5"/>
<text class="cls-5" transform="translate(42.07 31.3)"><tspan class="cls-6" x="0" y="0">LV</tspan><tspan class="cls-8" x="27.88" y="0">1</tspan></text>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 112.3 38.94">
<defs>
<style>
.cls-1 {
font-size: 19px;
}
.cls-1, .cls-2 {
stroke-width: 2px;
}
.cls-3 {
opacity: .18;
}
.cls-3, .cls-4, .cls-5 {
fill: #fff;
}
.cls-6 {
fill: #fdad00;
}
.cls-7 {
fill: #fdd231;
}
.cls-8 {
fill: #fdb300;
}
.cls-9 {
filter: url(#drop-shadow-1);
}
.cls-2 {
fill: url(#_未命名的渐变_77);
stroke: #333;
stroke-linejoin: round;
}
.cls-4 {
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke: #fff;
stroke-miterlimit: 10;
}
.cls-10 {
fill: #ea6c00;
}
.cls-5 {
opacity: .22;
}
.cls-11 {
fill: #feeeb7;
}
.cls-12 {
font-size: 25.34px;
letter-spacing: 0em;
}
</style>
<linearGradient id="_未命名的渐变_77" data-name="未命名的渐变 77" x1="24.93" y1="51.39" x2="87.37" y2="-11.05" gradientUnits="userSpaceOnUse">
<stop offset=".36" stop-color="#fdad00"/>
<stop offset=".63" stop-color="#f7931e"/>
</linearGradient>
<filter id="drop-shadow-1" filterUnits="userSpaceOnUse">
<feOffset dx="7" dy="7"/>
<feGaussianBlur result="blur" stdDeviation="5"/>
<feFlood flood-color="#000" flood-opacity=".4"/>
<feComposite in2="blur" operator="in"/>
<feComposite in="SourceGraphic"/>
</filter>
</defs>
<rect class="cls-2" x="1" y="3.78" width="110.3" height="32.77" rx="15.54" ry="15.54"/>
<text class="cls-4" transform="translate(43.59 28.04) scale(.87 1)"><tspan class="cls-1" x="0" y="0">LV</tspan><tspan class="cls-12" x="29.43" y="0">10</tspan></text>
<polygon class="cls-5" points="75.59 35.56 61.59 4.86 56.59 4.86 70.59 35.56 75.59 35.56"/>
<polygon class="cls-3" points="84.74 35.56 70.74 4.86 65.74 4.86 79.74 35.56 84.74 35.56"/>
<g class="cls-9">
<path class="cls-8" d="M8.42,1.15L19.29,0l10.87,1.15,8.42,11.3-19.29,22.47L0,12.45,8.42,1.15Z"/>
<path class="cls-10" d="M7.81,12.56l11.48,22.47L0,12.56H7.81Z"/>
<path class="cls-10" d="M30.77,12.56l-11.48,22.47L38.59,12.56h-7.81Z"/>
<path class="cls-6" d="M7.81,12.56H30.77l-11.48,22.47L7.81,12.56Z"/>
<path class="cls-7" d="M19.29,0L8.42,1.15l-.6,11.3L19.29,0Z"/>
<path class="cls-7" d="M19.29,0l10.87,1.15,.6,11.3L19.29,0Z"/>
<path class="cls-6" d="M38.59,12.56L30.17,1.25l.6,11.3h7.81Z"/>
<path class="cls-6" d="M0,12.56L8.42,1.25l-.6,11.3H0Z"/>
<path class="cls-11" d="M19.29,.1L7.81,12.56H30.77L19.29,.1Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 111.09 37.03">
<defs>
<style>
.cls-1 {
fill: #7ed321;
}
.cls-1, .cls-2 {
stroke-linecap: round;
stroke-width: 2.42px;
}
.cls-1, .cls-2, .cls-3 {
stroke-linejoin: round;
}
.cls-1, .cls-3 {
stroke: #333;
}
.cls-4 {
font-size: 18px;
}
.cls-4, .cls-3 {
stroke-width: 2px;
}
.cls-2 {
fill: none;
}
.cls-2, .cls-5 {
stroke: #fff;
}
.cls-6 {
font-size: 24px;
}
.cls-5 {
fill: #fff;
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke-miterlimit: 10;
}
.cls-3 {
fill: url(#_未命名的渐变_18);
}
</style>
<linearGradient id="_未命名的渐变_18" data-name="未命名的渐变 18" x1="87.16" y1="-9.17" x2="28.43" y2="49.56" gradientUnits="userSpaceOnUse">
<stop offset=".04" stop-color="#7ac943"/>
<stop offset=".56" stop-color="#40931e"/>
</linearGradient>
</defs>
<rect class="cls-3" x="5.5" y="4.65" width="104.58" height="31.08" rx="15.54" ry="15.54"/>
<text class="cls-5" transform="translate(43.86 26.65)"><tspan class="cls-4" x="0" y="0">LV</tspan><tspan class="cls-6" x="27.88" y="0">2</tspan></text>
<g>
<path class="cls-1" d="M19.45,1.22c7.32,.43,7.53,7.92,6.65,11,8.86-2.82,12.26,1.17,12.85,3.52,1.06,5.63-4.28,7.33-7.09,7.48,5.67,7.04,2.95,10.56,.89,11.44-6.38,1.76-10.04-2.79-11.08-5.28-2.84,5.98-7.68,6.31-9.75,5.72-6.03-1.76-3.4-8.07-1.33-11C2.08,22.69,.84,18.52,1.29,16.62c1.42-7.04,8.86-5.87,12.41-4.4C12.27,3.06,16.94,1.07,19.45,1.22Z"/>
<path class="cls-2" d="M21.09,20.15l-3.79,4.73"/>
<path class="cls-2" d="M15.41,19.2l5.68,.95"/>
<path class="cls-2" d="M20.15,13.52l.95,6.63"/>
<path class="cls-2" d="M26.78,18.25l-5.68,1.89"/>
<path class="cls-2" d="M21.09,20.15l4.73,3.79"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 109.65 40.33">
<defs>
<style>
.cls-1, .cls-2 {
stroke-width: 2.58px;
}
.cls-1, .cls-2, .cls-3 {
stroke-linejoin: round;
}
.cls-1, .cls-4 {
fill: #fff;
stroke: #fff;
}
.cls-5 {
font-size: 18px;
}
.cls-5, .cls-3 {
stroke-width: 2px;
}
.cls-2 {
fill: #f57887;
stroke: #000;
}
.cls-3 {
fill: url(#_未命名的渐变_22);
stroke: #333;
}
.cls-6 {
font-size: 24px;
}
.cls-4 {
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke-miterlimit: 10;
}
</style>
<linearGradient id="_未命名的渐变_22" data-name="未命名的渐变 22" x1="85.72" y1="-5.87" x2="26.99" y2="52.86" gradientUnits="userSpaceOnUse">
<stop offset=".15" stop-color="#f54e87"/>
<stop offset=".56" stop-color="#f57887"/>
</linearGradient>
</defs>
<rect class="cls-3" x="4.06" y="7.96" width="104.58" height="31.08" rx="15.54" ry="15.54"/>
<text class="cls-4" transform="translate(42.42 29.95)"><tspan class="cls-5" x="0" y="0">LV</tspan><tspan class="cls-6" x="27.88" y="0">3</tspan></text>
<g>
<path class="cls-2" d="M19.47,1.29l-5.45,5.45H6.75v7.27L1.29,19.47l5.45,5.45v7.27h7.27l5.45,5.45,5.45-5.45h7.27v-7.27l5.45-5.45-5.45-5.45V6.75h-7.27L19.47,1.29Z"/>
<path class="cls-1" d="M19.15,25.06c3.03,0,5.48-2.45,5.48-5.48s-2.45-5.48-5.48-5.48-5.48,2.45-5.48,5.48,2.45,5.48,5.48,5.48Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 107.88 38.22">
<defs>
<style>
.cls-1 {
fill: #ff931e;
}
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5 {
stroke-linejoin: round;
}
.cls-1, .cls-2, .cls-3, .cls-5 {
stroke-width: 2.58px;
}
.cls-1, .cls-2, .cls-5 {
stroke: #000;
}
.cls-6 {
font-size: 18px;
}
.cls-6, .cls-4 {
stroke-width: 2px;
}
.cls-2 {
fill: #29abe2;
}
.cls-7 {
filter: url(#drop-shadow-1);
}
.cls-8 {
font-size: 24px;
}
.cls-9 {
fill: #fff;
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke-miterlimit: 10;
}
.cls-9, .cls-3 {
stroke: #fff;
}
.cls-3 {
fill: none;
stroke-linecap: round;
}
.cls-4 {
fill: url(#_未命名的渐变_23);
stroke: #333;
}
.cls-5 {
fill: #d0021b;
}
</style>
<linearGradient id="_未命名的渐变_23" data-name="未命名的渐变 23" x1="83.95" y1="-7.98" x2="25.22" y2="50.75" gradientUnits="userSpaceOnUse">
<stop offset=".15" stop-color="#ff001c"/>
<stop offset=".56" stop-color="#ff4f00"/>
</linearGradient>
<filter id="drop-shadow-1" filterUnits="userSpaceOnUse">
<feOffset dx="7" dy="7"/>
<feGaussianBlur result="blur" stdDeviation="5"/>
<feFlood flood-color="#000" flood-opacity=".46"/>
<feComposite in2="blur" operator="in"/>
<feComposite in="SourceGraphic"/>
</filter>
</defs>
<rect class="cls-4" x="2.29" y="5.84" width="104.58" height="31.08" rx="15.54" ry="15.54"/>
<text class="cls-9" transform="translate(40.65 27.84)"><tspan class="cls-6" x="0" y="0">LV</tspan><tspan class="cls-8" x="27.88" y="0">4</tspan></text>
<g class="cls-7">
<path class="cls-5" d="M22.57,6.27c0-3.27-3.16-5.81-6.59-4.73-2.05,.65-3.37,2.65-3.37,4.8,0,5.31,0,17.77,0,23.08,0,2.15,1.31,4.16,3.37,4.8,3.44,1.08,6.59-1.45,6.59-4.73V6.27Z"/>
<path class="cls-1" d="M22.57,14.93l5.71,5.71c1.06,1.06,2.74,1.39,4.02,.61,1.86-1.14,2.07-3.61,.63-5.04L22.57,5.83V14.93Z"/>
<path class="cls-2" d="M12.61,20.64l-5.71-5.71c-1.06-1.06-2.74-1.39-4.02-.61-1.86,1.14-2.07,3.6-.63,5.04l10.37,10.37v-9.1Z"/>
<path class="cls-3" d="M17.59,6.54v1.24"/>
<path class="cls-3" d="M17.59,28.95h0"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 106.58 39.48">
<defs>
<style>
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5 {
stroke-linejoin: round;
}
.cls-1, .cls-3, .cls-4, .cls-5 {
stroke-width: 2.58px;
}
.cls-1, .cls-6 {
fill: #fff;
}
.cls-1, .cls-6, .cls-4 {
stroke: #fff;
}
.cls-2 {
fill: url(#_未命名的渐变_27);
stroke: #333;
}
.cls-2, .cls-7 {
stroke-width: 2px;
}
.cls-7 {
font-size: 18px;
}
.cls-3, .cls-4 {
fill: none;
stroke-linecap: round;
}
.cls-3, .cls-5 {
stroke: #000;
}
.cls-8 {
font-size: 24px;
}
.cls-6 {
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke-miterlimit: 10;
}
.cls-5 {
fill: #ed1e79;
}
</style>
<linearGradient id="_未命名的渐变_27" data-name="未命名的渐变 27" x1="82.66" y1="-6.72" x2="23.93" y2="52.01" gradientUnits="userSpaceOnUse">
<stop offset=".15" stop-color="#ed1e79"/>
<stop offset=".56" stop-color="#ed5c79"/>
</linearGradient>
</defs>
<rect class="cls-2" x="1" y="7.11" width="104.58" height="31.08" rx="15.54" ry="15.54"/>
<text class="cls-6" transform="translate(39.36 29.11)"><tspan class="cls-7" x="0" y="0">LV</tspan><tspan class="cls-8" x="27.88" y="0">5</tspan></text>
<g>
<path class="cls-5" d="M9.86,33.2l2.68-6.63c-3.67-2.47-6.08-6.69-6.08-11.5C6.46,7.46,12.53,1.29,20.02,1.29s13.56,6.17,13.56,13.78c0,4.8-2.42,9.03-6.08,11.5l2.68,6.63H9.86Z"/>
<path class="cls-4" d="M17.11,28.26v4.94"/>
<path class="cls-4" d="M23.29,28.26v4.94"/>
<path class="cls-1" d="M14.44,15.96c1.32,0,2.39-1.07,2.39-2.39s-1.07-2.39-2.39-2.39-2.39,1.07-2.39,2.39,1.07,2.39,2.39,2.39Z"/>
<path class="cls-1" d="M25.61,15.96c1.32,0,2.39-1.07,2.39-2.39s-1.07-2.39-2.39-2.39-2.39,1.07-2.39,2.39,1.07,2.39,2.39,2.39Z"/>
<path class="cls-3" d="M26.99,33.2h-7.41"/>
<path class="cls-3" d="M19.58,33.2h-6.18"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 108.33 35.38">
<defs>
<style>
.cls-1 {
fill: #50e3c2;
}
.cls-1, .cls-2 {
stroke: #000;
stroke-width: 2.58px;
}
.cls-1, .cls-2, .cls-3 {
stroke-linejoin: round;
}
.cls-4 {
font-size: 18px;
}
.cls-4, .cls-3 {
stroke-width: 2px;
}
.cls-5 {
filter: url(#drop-shadow-1);
}
.cls-2 {
fill: none;
stroke-linecap: round;
}
.cls-6 {
font-size: 24px;
}
.cls-7 {
fill: #fff;
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke: #fff;
stroke-miterlimit: 10;
}
.cls-3 {
fill: url(#_未命名的渐变_30);
stroke: #333;
}
</style>
<linearGradient id="_未命名的渐变_30" data-name="未命名的渐变 30" x1="84.4" y1="-10.82" x2="25.67" y2="47.9" gradientUnits="userSpaceOnUse">
<stop offset=".15" stop-color="#50e3c2"/>
<stop offset=".56" stop-color="#50b1c2"/>
</linearGradient>
<filter id="drop-shadow-1" filterUnits="userSpaceOnUse">
<feOffset dx="7" dy="7"/>
<feGaussianBlur result="blur" stdDeviation="5"/>
<feFlood flood-color="#000" flood-opacity=".46"/>
<feComposite in2="blur" operator="in"/>
<feComposite in="SourceGraphic"/>
</filter>
</defs>
<rect class="cls-3" x="2.74" y="3" width="104.58" height="31.08" rx="15.54" ry="15.54"/>
<text class="cls-7" transform="translate(41.1 25)"><tspan class="cls-4" x="0" y="0">LV</tspan><tspan class="cls-6" x="27.88" y="0">6</tspan></text>
<g class="cls-5">
<path class="cls-1" d="M1.29,8.39L19.06,1.29l17.77,7.1-17.77,7.1L1.29,8.39Z"/>
<path class="cls-2" d="M37.33,9.02v7.85"/>
<path class="cls-2" d="M9.84,11.64v11.05s4.12,4.05,9.82,4.05,9.82-4.05,9.82-4.05V11.64"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 114.15 38.61">
<defs>
<style>
.cls-1 {
fill: url(#_未命名的渐变_57);
stroke: #333;
stroke-linejoin: round;
}
.cls-1, .cls-2 {
stroke-width: 2px;
}
.cls-3 {
fill: #fdbd39;
}
.cls-4 {
fill: none;
isolation: isolate;
opacity: .9;
stroke: #ee6723;
stroke-width: 1.2px;
}
.cls-2 {
font-size: 19px;
}
.cls-5 {
fill: #ee6723;
}
.cls-6 {
opacity: .18;
}
.cls-6, .cls-7, .cls-8 {
fill: #fff;
}
.cls-9 {
font-size: 25.34px;
}
.cls-10 {
filter: url(#drop-shadow-1);
}
.cls-7 {
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke: #fff;
stroke-miterlimit: 10;
}
.cls-8 {
opacity: .22;
}
.cls-11 {
fill: #f69833;
}
.cls-12 {
fill: #fecf33;
}
</style>
<linearGradient id="_未命名的渐变_57" data-name="未命名的渐变 57" x1="89.22" y1="-11.38" x2="26.78" y2="51.06" gradientUnits="userSpaceOnUse">
<stop offset=".25" stop-color="#9e00c5"/>
<stop offset=".67" stop-color="#f7931e"/>
</linearGradient>
<filter id="drop-shadow-1" filterUnits="userSpaceOnUse">
<feOffset dx="7" dy="7"/>
<feGaussianBlur result="blur" stdDeviation="5"/>
<feFlood flood-color="#000" flood-opacity=".46"/>
<feComposite in2="blur" operator="in"/>
<feComposite in="SourceGraphic"/>
</filter>
</defs>
<rect class="cls-1" x="2.85" y="3.45" width="110.3" height="32.77" rx="15.54" ry="15.54"/>
<text class="cls-7" transform="translate(46.21 27.71)"><tspan class="cls-2" x="0" y="0">LV</tspan><tspan class="cls-9" x="29.43" y="0">7</tspan></text>
<g class="cls-10">
<path class="cls-3" d="M6.39,23.21l-2.17,.79-.12,6.45,7.5-2.74c-2.58-.86-4.45-2.38-5.22-4.5Z"/>
<path class="cls-11" d="M7.52,16.39L0,19.14l4.22,4.87,2.17-.79c-.76-2.11-.29-4.49,1.13-6.82Z"/>
<path class="cls-12" d="M20.18,7.08c-9.58,3.5-15.75,10.72-13.79,16.13L41.07,10.54c-1.96-5.41-11.32-6.95-20.89-3.46Z"/>
<path class="cls-5" d="M27.28,26.67c9.58-3.5,15.76-10.69,13.79-16.13L6.39,23.21c1.97,5.44,11.32,6.96,20.89,3.46Z"/>
<path class="cls-11" d="M41.07,10.54L6.39,23.21c.68,1.87,3.27,2.8,6.9,2.8s8.08-.88,12.81-2.6c9.58-3.5,16.32-9.16,14.97-12.86Z"/>
<path class="cls-3" d="M34.17,7.74c-3.54,0-8.08,.88-12.81,2.6-9.58,3.5-16.32,9.16-14.97,12.86L41.07,10.54c-.68-1.87-3.27-2.79-6.9-2.79Z"/>
<path class="cls-4" d="M25.08,5.72c-.91-3.22-1.98-5.2-2.97-5.12-1.87,.17-2.74,7.59-1.94,16.59,.8,9,2.95,16.16,4.82,15.99,1.08-.1,1.82-2.61,2.11-6.45,.06-.02,.13-.05,.19-.07,1.67-.61,3.24-1.33,4.68-2.14,1.36,3.01,1.8,5.18,.99,5.74-1.53,1.08-6.99-4.04-12.18-11.43C15.57,11.46,12.6,4.59,14.14,3.52c.89-.63,3.12,.84,5.84,3.64"/>
</g>
<polygon class="cls-8" points="77.44 35.23 63.44 4.53 58.44 4.53 72.44 35.23 77.44 35.23"/>
<polygon class="cls-6" points="86.59 35.23 72.59 4.53 67.59 4.53 81.59 35.23 86.59 35.23"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 113.69 38.29">
<defs>
<style>
.cls-1 {
fill: url(#_未命名的渐变_62);
stroke: #333;
stroke-linejoin: round;
}
.cls-1, .cls-2 {
stroke-width: 2px;
}
.cls-2 {
font-size: 19px;
}
.cls-3 {
opacity: .18;
}
.cls-3, .cls-4, .cls-5 {
fill: #fff;
}
.cls-6 {
font-size: 25.34px;
}
.cls-7 {
fill: #00ad45;
}
.cls-8 {
fill: #b3dcff;
}
.cls-4 {
font-family: A023-SounsoUndividedad, 'A023-Sounso Undividedad';
stroke: #fff;
stroke-miterlimit: 10;
}
.cls-5 {
opacity: .22;
}
.cls-9 {
fill: #5ecc62;
}
</style>
<linearGradient id="_未命名的渐变_62" data-name="未命名的渐变 62" x1="88.76" y1="-11.69" x2="26.33" y2="50.74" gradientUnits="userSpaceOnUse">
<stop offset=".12" stop-color="#b3dcff"/>
<stop offset=".6" stop-color="#009245"/>
</linearGradient>
</defs>
<rect class="cls-1" x="2.39" y="3.14" width="110.3" height="32.77" rx="15.54" ry="15.54"/>
<text class="cls-4" transform="translate(45.75 27.4)"><tspan class="cls-2" x="0" y="0">LV</tspan><tspan class="cls-6" x="29.43" y="0">8</tspan></text>
<polygon class="cls-5" points="76.98 34.91 62.98 4.21 57.98 4.21 71.98 34.91 76.98 34.91"/>
<polygon class="cls-3" points="86.13 34.91 72.13 4.21 67.13 4.21 81.13 34.91 86.13 34.91"/>
<g>
<path class="cls-8" d="M20.49,0C9.46,0,.83,14.53,0,26.8c3.46,6.22,11.81,8.59,20.49,8.59s17.03-2.34,20.48-8.59C40.16,14.53,31.52,0,20.49,0Z"/>
<path class="cls-9" d="M33.26,15.26c-2.42-3.24-5.51-6.67-7.3-6.67s-6.79,9.17-9.12,9.17-3.49-4.17-6.31-4.16c-3.85,0-7.61,11.72-7.61,11.72,0,0,1.56,7.15,17.86,7.15s17.61-7.03,17.61-7.03c-1.18-3.65-2.91-7.09-5.13-10.19Z"/>
<path d="M11.75,24.35c0-.42,.12-.84,.35-1.19,.23-.35,.55-.63,.93-.79,.38-.16,.79-.2,1.19-.12,.4,.08,.77,.29,1.06,.59,.29,.3,.49,.68,.57,1.09,.08,.41,.04,.84-.12,1.24-.16,.39-.42,.72-.76,.96-.34,.23-.74,.36-1.15,.36-.55,0-1.07-.23-1.46-.63-.39-.4-.6-.94-.6-1.51Zm13.34,0c0,.42,.12,.84,.35,1.19s.55,.63,.93,.79c.38,.16,.79,.2,1.19,.12s.77-.29,1.06-.59c.29-.3,.49-.68,.57-1.09,.08-.41,.04-.84-.12-1.24-.16-.39-.42-.72-.76-.96-.34-.23-.74-.36-1.15-.36-.27,0-.54,.06-.79,.16-.25,.11-.48,.27-.67,.46-.19,.2-.34,.43-.45,.69-.1,.26-.16,.54-.16,.82Zm-.37,3.13c.06-.15,.07-.32,.01-.48-.05-.16-.16-.28-.3-.36-.14-.08-.31-.1-.46-.05-.15,.04-.29,.14-.37,.28-.33,.53-.78,.95-1.32,1.24-.54,.28-1.14,.41-1.74,.38-.62,.01-1.23-.13-1.79-.42-.55-.29-1.03-.71-1.39-1.23-.09-.13-.23-.23-.39-.26-.16-.03-.32,0-.46,.08-.14,.09-.24,.22-.28,.38s-.03,.33,.04,.48c.47,.71,1.11,1.3,1.85,1.69s1.57,.6,2.41,.58c.83,.04,1.66-.16,2.4-.56,.74-.41,1.35-1.01,1.79-1.75Z"/>
<path class="cls-7" d="M17.13,21.69c-2.53,0-3.83-2.58-4.54-4.18-.63-1.43-.75-3.71-2.54-3.77,.16-.07,.33-.11,.51-.11,2.82,0,4.01,4.16,6.31,4.16s7.31-9.2,9.09-9.2c.4,.02,.78,.14,1.13,.34-.18-.03-.36-.03-.53,.03-.17,.05-.33,.15-.46,.27-.57,.61-5.83,12.46-8.97,12.46Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1681972447512" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="49893" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400"><path d="M688.4352 397.824L511.7952 610.2016l-177.2544-212.992h-91.136v34.5088a30.8224 30.8224 0 0 1 28.9792 12.288v3.6864c38.8096 49.8688 191.488 223.4368 234.5984 276.992 43.1104-54.1696 195.1744-227.7376 234.5984-278.2208 6.656-9.1136 17.7152-13.824 28.9792-12.288v-39.424h-86.2208c-0.2048 0 6.5536 0 4.096 3.072z m259.7888-52.9408c32.6656 38.7072 32.6656 95.4368 0 134.2464L736.4608 709.3248 580.096 877.9776a85.7088 85.7088 0 0 1-67.6864 34.5088 86.00576 86.00576 0 0 1-67.6864-34.5088L293.2736 714.8544 75.8784 479.6416a100.97664 100.97664 0 0 1 0-132.4032l100.352-111.4112 83.1488-90.5216a88.40192 88.40192 0 0 1 68.9152-33.8944h369.3568a90.8288 90.8288 0 0 1 66.4576 33.28C798.72 179.2 873.1648 261.7344 918.1184 310.3744l30.1056 34.5088z" fill="#409EFF" p-id="49894"></path></svg>
<svg t="1730718997526" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10455" width="200" height="200"><path d="M593.92 125.013333c25.173333 30.293333 64.426667 44.8 103.253333 37.973334 67.413333-11.946667 129.28 40.533333 129.28 109.653333 0 39.68 20.906667 76.373333 55.04 96.426667 59.306667 34.56 73.386667 114.773333 29.44 168.106666a113.066667 113.066667 0 0 0-19.2 109.653334c23.466667 64.853333-17.066667 135.68-84.48 147.626666-38.826667 6.826667-70.826667 34.133333-84.053333 71.68-23.466667 64.853333-98.986667 93.013333-158.293333 58.453334-34.133333-19.626667-75.946667-19.626667-110.08 0-59.306667 34.56-134.826667 6.826667-158.293334-58.453334a112.170667 112.170667 0 0 0-84.053333-71.68C145.066667 782.506667 104.96 711.68 128 646.826667c13.226667-37.12 5.973333-78.933333-19.2-109.653334-43.946667-52.906667-29.866667-133.12 29.44-168.106666a111.786667 111.786667 0 0 0 55.04-96.426667c0-69.12 61.866667-121.6 129.28-109.653333 38.826667 6.826667 78.08-7.68 103.253333-37.973334a109.312 109.312 0 0 1 168.533334 0z" fill="#E2961B" p-id="10456"></path><path d="M473.173333 661.333333c-32.853333 0-65.28-12.373333-90.453333-37.546666l-75.946667-75.946667c-17.066667-17.066667-17.066667-45.226667 0-62.293333 17.066667-17.066667 45.226667-17.066667 62.293334 0l75.946666 75.946666c15.36 15.36 40.96 15.36 56.32 0l153.6-153.6c17.066667-17.066667 45.226667-17.066667 62.293334 0 17.066667 17.066667 17.066667 45.226667 0 62.293334l-153.6 153.6a128 128 0 0 1-90.453334 37.546666z" fill="#FFFFFF" p-id="10457"></path></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -12,9 +12,4 @@
font-weight: 600;
margin-bottom: 10px;
}
}

View File

@@ -9,7 +9,6 @@
v-model:value="commentContent"
@keyup.ctrl.enter="showSlideCaptcha"
:placeholder="t('comment.placeholder')" allow-clear :showCount="false"/>
<AFlex :vertical="false" align="center" justify="space-between" class="comment-actions"
v-if="showCommentActions">
<AFlex :vertical="false" align="center">
@@ -188,8 +187,12 @@ async function commentSubmit(point: any) {
likes: result.data.likes,
author: result.data.author,
location: result.data.location,
is_liked:false,
is_liked: false,
};
if (!comment.commentList.comments) {
comment.commentList.comments = []; // 初始化 comments 数组
}
comment.commentList.comments.unshift(tmpData);
commentContent.value = "";
await comment.clearFileList();

View File

@@ -25,22 +25,33 @@
<div class="reply-item" v-for="(item, index) in comment.commentList.comments" :key="index">
<AFlex :vertical="false" style="margin-top: 5px">
<!-- 评论头像 -->
<ABadge :offset="[0,0]" :dot="false">
<ABadge :offset="[0,40]" :dot="false">
<template #count v-if="true">
<img src="/level_icon/up.svg" style="width: 20px;height: 20px;" alt="lv2">
<img src="/level_icon/up.svg" style="width: 25px;height: 25px;" alt="up">
</template>
<AFlex :vertical="true" class="reply-avatar" v-if="item.avatar">
<AAvatar :size="50" class="reply-avatar-img" shape="circle" :src="item.avatar"/>
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}" @openChange="(open: boolean)=>{
console.log(open);
}">
<template #content>
<UserInfoCard :user="item" :padding="0"/>
</template>
<AAvatar :size="50" class="reply-avatar-img" shape="circle" :src="item.avatar"/>
</Popover>
</AFlex>
</ABadge>
<!-- 评论内容 -->
<AFlex :vertical="true" class="reply-content">
<AFlex :vertical="true">
<AFlex :vertical="false" align="center" justify="flex-start">
<span class="reply-name">{{ item.nickname }}</span>
<img src="/level_icon/3/lv5.png" class="reply-level-icon" alt="lv1">
<img src="/level_icon/4/4.png" class="reply-level-icon" alt="lv2">
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
<template #content>
<UserInfoCard :user="item" :padding="0"/>
</template>
<span class="reply-name">{{ item.nickname }}</span>
</Popover>
<img src="/level_icon/icon/lv1.png" class="reply-level-icon" alt="level">
</AFlex>
<AFlex :vertical="false" align="flex-end" justify="space-between">
<AFlex :vertical="false" align="center" justify="space-between">
@@ -56,11 +67,14 @@
</div>
<AFlex :vertical="false" align="center" class="reply-images" v-if="item.images">
<AImagePreviewGroup>
<AImage :width="80" :height="80" v-for="(image, index) in item.images" :key="index" :src="image">
<template #previewMask>
<EyeOutlined style="font-size: 20px;"/>
</template>
</AImage>
<ASpace direction="horizontal">
<AImage :width="80" :height="80" v-for="(image, index) in item.images" :key="index"
:src="image">
<template #previewMask>
<EyeOutlined style="font-size: 20px;"/>
</template>
</AImage>
</ASpace>
</AImagePreviewGroup>
</AFlex>
<AFlex :vertical="false" justify="space-between" align="center">
@@ -160,6 +174,7 @@ import {useRouter} from "vue-router";
import ReplyInput from "@/components/CommentReply/src/ReplyInput/ReplyInput.vue";
import ReplyList from "@/components/CommentReply/src/ReplyList/ReplyList.vue";
import MessageReport from "@/components/CommentReply/src/MessageReport/MessageReport.vue";
import UserInfoCard from "@/components/CommentReply/src/UserInfoCard/UserInfoCard.vue";
const {t} = useI18n();

View File

@@ -21,6 +21,10 @@
.reply-avatar {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border-radius: 50%;
.reply-popover{
padding: 0;
}
}
.reply-avatar-img {
@@ -53,7 +57,7 @@
}
.reply-level-icon {
width: 40px;
width: 30px;
margin-left: 5px;
cursor: pointer;
}

View File

@@ -208,6 +208,9 @@ async function replySubmit(point: any) {
is_liked: false,
reply_username: props.item.nickname,
};
if (!comment.replyList.comments) {
comment.replyList.comments = []; // 初始化 comments 数组
}
comment.replyList.comments.unshift(tmpData);
comment.commentMap[props.item.id].reply_count++;
comment.closeReplyInput();

View File

@@ -4,17 +4,36 @@
<AFlex :vertical="true" v-if="comment.replyList.comments">
<AFlex :vertical="false" style="margin-top: 5px" v-for="(child, index) in comment.replyList.comments"
:key="index">
<AFlex :vertical="true" >
<AAvatar :size="40" shape="circle" class="reply-item-child-avatar" :src="child.avatar"/>
<AFlex :vertical="true">
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
<template #content>
<UserInfoCard :user="child" :padding="0"/>
</template>
<ABadge :offset="[0,35]" :dot="false">
<template #count v-if="true">
<img src="/level_icon/up.svg" style="width: 20px;height: 20px;" alt="up">
</template>
<AAvatar :size="40" shape="circle" class="reply-item-child-avatar" :src="child.avatar"/>
</ABadge>
</Popover>
</AFlex>
<AFlex :vertical="true" class="reply-item-child-content">
<AFlex :vertical="true">
<AFlex :vertical="false" align="center">
<span class="reply-name-child">{{ child.nickname }}</span>
<span
class="reply-at">@{{ child.reply_username }}</span>
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
<template #content>
<UserInfoCard :user="child" :padding="0"/>
</template>
<span class="reply-name-child">{{ child.nickname }}</span>
</Popover>
<Popover :arrow="false" :offset-x="170" :contentStyle="{padding: 0}">
<template #content>
<UserInfoCard :user="child" :padding="0"/>
</template>
<span
class="reply-at">@{{ child.reply_username }}</span>
</Popover>
<a-tag color="cyan" class="reply-tag-child" size="small">Lv.5</a-tag>
<!-- <a-tag color="red" class="reply-tag" size="small">UP</a-tag>-->
</AFlex>
<AFlex :vertical="false" align="flex-end" justify="space-between">
<AFlex :vertical="false" align="center" justify="space-between">
@@ -30,11 +49,14 @@
<AFlex :vertical="false" align="center" class="reply-images" v-if="child.images">
<AImagePreviewGroup>
<AImagePreviewGroup>
<AImage :width="80" :height="80" v-for="(image, index) in child.images" :key="index" :src="image">
<template #previewMask>
<EyeOutlined style="font-size: 20px;"/>
</template>
</AImage>
<ASpace direction="horizontal">
<AImage :width="80" :height="80" v-for="(image, index) in child.images" :key="index"
:src="image">
<template #previewMask>
<EyeOutlined style="font-size: 20px;"/>
</template>
</AImage>
</ASpace>
</AImagePreviewGroup>
</AImagePreviewGroup>
</AFlex>
@@ -123,6 +145,7 @@ import {useI18n} from "vue-i18n";
import useStore from "@/store";
import ReplyReply from "@/components/CommentReply/src/ReplyReplyInput/ReplyReply.vue";
import {useThrottleFn} from "@vueuse/core";
import UserInfoCard from "@/components/CommentReply/src/UserInfoCard/UserInfoCard.vue";
const {t} = useI18n();

View File

@@ -13,6 +13,7 @@
.reply-item-child-avatar {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.reply-item-child-content {

View File

@@ -211,6 +211,9 @@ async function replyReplySubmit(point: any) {
reply_username: props.item.nickname,
reply_to: result.data.reply_to,
};
if (!comment.replyList.comments) {
comment.replyList.comments = []; // 初始化 comments 数组
}
comment.replyList.comments.unshift(tmpData);
comment.commentMap[props.item.id].reply_count++;
replyReplyContent.value = "";

View File

@@ -0,0 +1,72 @@
<template>
<div class="user-info-card-main">
<AFlex :vertical="true" justify="flex-start">
<div>
<img src="@/assets/images/background.jpg" class="user-info-card-image" :width="350" :height="100" alt=""/>
</div>
<AFlex :vertical="false" class="user-info-card-name" style="">
<AFlex :vertical="true" justify="flex-start">
<AAvatar :size="48" :src="users.user.userInfo.avatar" class="user-info-card-avatar"/>
</AFlex>
<AFlex :vertical="true" justify="flex-start" class="user-info-card-content-container">
<AFlex :vertical="false" align="center">
<span class="user-info-card-name-text">
{{ props.user.nickname }}
</span>
<img src="/level_icon/icon/lv1.png" class="user-info-card-level-icon" alt="level">
</AFlex>
<AFlex :vertical="false" align="center" justify="space-between" style="width: 250px">
<span class="user-info-card-info-text">
416关注
</span>
<span class="user-info-card-info-text">
360.0万粉丝
</span>
<span class="user-info-card-info-text">
5466.5万获赞
</span>
</AFlex>
<AFlex :vertical="false" align="center" style="width: 270px">
<span class="user-info-card-info-text">
bilibili个人认证bilibili 2020百大UP主知名搞笑UP主<br/>
商务合作WX: 13311286278
</span>
</AFlex>
<AFlex :vertical="false" align="center" justify="flex-start">
<AButton type="primary">
<template #icon>
<PlusOutlined/>
</template>
</AButton>
<AButton type="default" style="margin-left: 10px">
<template #icon>
<SendOutlined/>
</template>
</AButton>
</AFlex>
</AFlex>
</AFlex>
</AFlex>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
user: {
type: Object,
required: true
}
});
import useStore from "@/store";
const users = useStore().user;
</script>
<style scoped lang="scss" src="./index.scss">
</style>

View File

@@ -0,0 +1,34 @@
.user-info-card-main {
width: 350px;
min-height: 250px;
.user-info-card-image {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.user-info-card-name {
padding-top: 5px;
padding-left: 10px;
.user-info-card-content-container {
margin-left: 10px;
.user-info-card-name-text {
font-size: 15px;
font-weight: 600;
}
.user-info-card-level-icon {
width: 40px;
margin-left: 5px;
cursor: pointer;
}
.user-info-card-info-text{
font-size: 12px;
color: #9499a0;
}
}
}
}

View File

@@ -0,0 +1,149 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import Tooltip from '../tooltip/Tooltip.vue';
import { useResizeObserver } from '../utils';
interface Props {
maxWidth?: string | number // 文本最大宽度,单位 px
tooltipMaxWidth?: string | number // 弹出提示最大宽度,单位 px默认为 maxWidth + 24
line?: number // 最大行数
expand?: boolean // 是否启用点击文本展开全部
tooltip?: boolean // 是否启用文本提示框,可自定义设置弹出提示内容 boolean | slot
}
const props = withDefaults(defineProps<Props>(), {
maxWidth: '100%',
tooltipMaxWidth: undefined,
line: undefined,
expand: false,
tooltip: true
});
const showTooltip = ref(false); // 是否显示提示框
const showExpand = ref(false); // 是否可以启用点击展开
const ellipsisRef = ref();
const ellipsisLine = ref(); // 行数
const stopObservation = ref(false);
const emit = defineEmits(['expandChange']);
const textMaxWidth = computed(() => {
if (typeof props.maxWidth === 'number') {
return `${props.maxWidth}px`;
}
return props.maxWidth;
});
watch(
() => props.line,
(to) => {
if (to !== undefined) {
ellipsisLine.value = to;
} else {
ellipsisLine.value = 'none';
}
},
{
immediate: true
}
);
watch(
() => [props.maxWidth, props.line, props.tooltip],
() => {
updateTooltipShow();
},
{
deep: true,
flush: 'post'
}
);
useResizeObserver(ellipsisRef, () => {
if (stopObservation.value) {
setTimeout(() => {
stopObservation.value = false;
});
} else {
updateTooltipShow();
}
});
onMounted(() => {
updateTooltipShow();
});
function updateTooltipShow() {
const scrollWidth = ellipsisRef.value.scrollWidth;
const scrollHeight = ellipsisRef.value.scrollHeight;
const clientWidth = ellipsisRef.value.clientWidth;
const clientHeight = ellipsisRef.value.clientHeight;
if (scrollWidth > clientWidth || scrollHeight > clientHeight) {
if (props.expand) {
showExpand.value = true;
}
if (props.tooltip) {
showTooltip.value = true;
}
} else {
if (props.expand) {
showExpand.value = false;
}
if (props.tooltip) {
showTooltip.value = false;
}
}
}
function onExpand() {
stopObservation.value = true;
if (ellipsisLine.value !== 'none') {
ellipsisLine.value = 'none';
if (props.tooltip && showTooltip.value) {
showTooltip.value = false;
}
emit('expandChange', true);
} else {
ellipsisLine.value = props.line ?? 'none';
if (props.tooltip && !showTooltip.value) {
showTooltip.value = true;
}
emit('expandChange', false);
}
}
</script>
<template>
<Tooltip
:style="`max-width: ${textMaxWidth}`"
:max-width="tooltipMaxWidth || `calc(${textMaxWidth} + 24px)`"
:content-style="{ maxWidth: textMaxWidth }"
:tooltip-style="{ padding: '8px 12px' }"
:transition-duration="200"
v-bind="$attrs"
>
<template #tooltip>
<slot v-if="showTooltip" name="tooltip">
<slot></slot>
</slot>
</template>
<div
ref="ellipsisRef"
class="m-ellipsis"
:class="[line ? 'ellipsis-line' : 'not-ellipsis-line', { 'ellipsis-cursor-pointer': showExpand }]"
:style="`--ellipsis-max-width: ${textMaxWidth}; --ellipsis-line: ${ellipsisLine};`"
@click="showExpand ? onExpand() : () => false"
>
<slot></slot>
</div>
</Tooltip>
</template>
<style lang="less" scoped>
.m-ellipsis {
overflow: hidden;
cursor: text;
max-width: var(--ellipsis-max-width);
}
.ellipsis-line {
display: -webkit-inline-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--ellipsis-line);
}
.not-ellipsis-line {
display: inline-block;
vertical-align: bottom;
white-space: nowrap;
text-overflow: ellipsis;
}
.ellipsis-cursor-pointer {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
import { computed } from 'vue';
interface Gradient {
from: string
to: string
deg?: number | string // 渐变角度,默认 252单位 deg
}
interface Props {
gradient?: string | Gradient // 文字渐变色参数
size?: number | string // 文字大小,不指定单位时,默认单位 px
weight?: number // 文字粗细
type?: 'primary' | 'info' | 'success' | 'warning' | 'error' // 渐变文字的类型
}
const props = withDefaults(defineProps<Props>(), {
gradient: undefined,
size: 14,
weight: 400,
type: 'primary'
});
enum TypeStartColor {
primary = 'rgba(22, 199, 255, 0.6)',
info = 'rgba(22, 199, 255, 0.6)',
success = 'rgba(82, 196, 26, 0.6)',
warning = 'rgba(250, 173, 20, 0.6)',
error = 'rgba(255, 77, 79, 0.6)'
}
enum TypeEndColor {
primary = '#1677FF',
info = '#1677FF',
success = '#52c41a',
warning = '#faad14',
error = '#ff4d4f'
}
const gradientText = computed(() => {
if (typeof props.gradient === 'string') {
return {
backgroundImage: props.gradient
};
}
return {};
});
const rotate = computed(() => {
if (typeof props.gradient === 'object' && props.gradient.deg) {
return isNumber(props.gradient.deg) ? `${props.gradient.deg}deg` : props.gradient.deg;
}
return '252deg';
});
const colorStart = computed(() => {
if (typeof props.gradient === 'object') {
return props.gradient.from;
} else {
return TypeStartColor[props.type];
}
});
const colorEnd = computed(() => {
if (typeof props.gradient === 'object') {
return props.gradient.to;
} else {
return TypeEndColor[props.type];
}
});
const fontSize = computed(() => {
if (typeof props.size === 'number') {
return `${props.size}px`;
}
if (typeof props.size === 'string') {
return props.size;
}
return '14px';
});
function isNumber(value: string | number): boolean {
return typeof value === 'number';
}
</script>
<template>
<span
class="m-gradient-text"
:style="[
`--rotate: ${rotate}; --color-start: ${colorStart}; --color-end: ${colorEnd}; --font-size: ${fontSize}; --font-weight: ${weight};`,
gradientText
]"
>
<slot></slot>
</span>
</template>
<style lang="less" scoped>
.m-gradient-text {
display: inline-block;
font-size: var(--font-size);
font-weight: var(--font-weight);
line-height: 1.5714285714285714;
-webkit-background-clip: text;
background-clip: text;
color: #0000;
white-space: nowrap;
background-image: linear-gradient(var(--rotate), var(--color-start) 0%, var(--color-end) 100%);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed} from 'vue';
import {useSlotsExist} from '../utils';
interface Props {
title?: string // 卡片标题 string | slot
titleStyle?: CSSProperties // 卡片标题样式
content?: string // 卡片内容 string | slot
contentStyle?: CSSProperties // 卡片内容样式
tooltipStyle?: CSSProperties // 设置弹出提示的样式
offsetX?: number // 水平偏移量
padding?: string | number // 弹出提示的内边距
}
const props = withDefaults(defineProps<Props>(), {
title: undefined,
titleStyle: () => ({}),
content: undefined,
contentStyle: () => ({}),
tooltipStyle: () => ({}),
offsetX: 0, // 默认偏移量为0
padding: '0px' // 默认内边距为0
});
const slotsExist = useSlotsExist(['title', 'content']);
const showTitle = computed(() => {
return slotsExist.title || props.title;
});
const showContent = computed(() => {
return slotsExist.content || props.content;
});
</script>
<template>
<Tooltip
max-width="auto"
bg-color="#fff"
:tooltip-style="{
padding: props.padding, // 使用传入的 padding
borderRadius: '8px',
textAlign: 'start',
transform: `translate(${props.offsetX}px, 0)`,
...tooltipStyle
}"
:transition-duration="200"
v-bind="$attrs"
>
<template #tooltip>
<div class="arrow" :style="{ transform: `translateX(${props.offsetX - 10}px)` }"></div>
<div v-if="showTitle" class="popover-title" :class="{ mb8: showContent }" :style="titleStyle">
<slot name="title">{{ title }}</slot>
</div>
<div v-if="showContent" class="popover-content" :style="contentStyle">
<slot name="content">{{ content }}</slot>
</div>
</template>
<slot></slot>
</Tooltip>
</template>
<style lang="less" scoped>
.popover-title {
min-width: 176px;
color: rgba(0, 0, 0, 0.88);
font-weight: 600;
}
.mb8 {
margin-bottom: 8px;
}
.popover-content {
color: rgba(0, 0, 0, 0.88);
}
.arrow {
position: absolute;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff; /* 箭头的颜色 */
left: 50%; /* 使箭头在中间 */
transform: translateX(-50%); /* 向左移动箭头宽度的一半 */
}
</style>

View File

@@ -0,0 +1,383 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed, onMounted, ref} from 'vue';
import {debounce, useEventListener, useMutationObserver} from '../utils';
interface Props {
contentClass?: string // 内容 div 的类名
contentStyle?: CSSProperties // 内容 div 的样式
size?: number // 滚动条的大小,单位 px
trigger?: 'hover' | 'none' // 显示滚动条的时机,'none' 表示一直显示
autoHide?: boolean // 是否自动隐藏滚动条,仅当 trigger: 'hover' 时生效true: hover且不滚动时自动隐藏滚动时自动显示false: hover时始终显示
delay?: number // 滚动条自动隐藏的延迟时间,单位 ms
horizontal?: boolean // 是否使用横向滚动
}
const props = withDefaults(defineProps<Props>(), {
contentClass: undefined,
contentStyle: () => ({}),
size: 5,
trigger: 'hover',
autoHide: true,
delay: 1000,
horizontal: false
});
const scrollbarRef = ref();
const containerRef = ref();
const contentRef = ref();
const railVerticalRef = ref();
const railHorizontalRef = ref();
const showTrack = ref(false);
const containerScrollHeight = ref(0); // 滚动区域高度,包括溢出高度
const containerScrollWidth = ref(0); // 滚动区域宽度,包括溢出宽度
const containerClientHeight = ref(0); // 滚动区域高度,不包括溢出高度
const containerClientWidth = ref(0); // 滚动区域宽度,不包括溢出宽度
const containerHeight = ref(0); // 容器高度
const containerWidth = ref(0); // 容器宽度
const contentHeight = ref(0); // 内容高度
const contentWidth = ref(0); // 内容宽度
const railHeight = ref(0); // 滚动条高度
const railWidth = ref(0); // 滚动条宽度
const containerScrollTop = ref(0); // 垂直滚动距离
const containerScrollLeft = ref(0); // 水平滚动距离
const trackYPressed = ref(false); // 垂直滚动条是否被按下
const trackXPressed = ref(false); // 水平滚动条是否被按下
const mouseLeave = ref(false); // 鼠标在按下滚动条并拖动时是否离开滚动区域
const memoYTop = ref<number>(0); // 鼠标选中并按下垂直滚动条时已滚动的垂直距离
const memoXLeft = ref<number>(0); // 鼠标选中并按下水平滚动条时已滚动的水平距离
const memoMouseY = ref<number>(0); // 鼠标选中并按下垂直滚动条时的鼠标 Y 坐标
const memoMouseX = ref<number>(0); // 鼠标选中并按下水平滚动条时的鼠标 X 坐标
const horizontalContentStyle = {width: 'fit-content'}; // 水平滚动时内容区域默认样式
const trackHover = ref(false); // 鼠标是否在滚动条上
const trackLeave = ref(false); // 鼠标在按下滚动条并拖动时是否离开滚动条
const emit = defineEmits(['scroll']);
const autoShowTrack = computed(() => {
return props.trigger === 'hover' && props.autoHide;
});
const isYScroll = computed(() => {
// 是否存在垂直滚动
return containerScrollHeight.value > containerClientHeight.value;
});
const isXScroll = computed(() => {
// 是否存在水平滚动
return containerScrollWidth.value > containerClientWidth.value;
});
const isScroll = computed(() => {
// 是否存在滚动,水平或垂直
return isYScroll.value || (props.horizontal && isXScroll.value);
});
const trackHeight = computed(() => {
// 垂直滚动条高度
if (isYScroll.value) {
if (containerHeight.value && contentHeight.value && railHeight.value) {
const value = Math.min(
containerHeight.value,
(railHeight.value * containerHeight.value) / contentHeight.value + 1.5 * props.size
);
return Number(value.toFixed(4));
}
}
return 0;
});
const trackTop = computed(() => {
// 滚动条垂直偏移
if (containerHeight.value && contentHeight.value && railHeight.value) {
return (
(containerScrollTop.value / (contentHeight.value - containerHeight.value)) *
(railHeight.value - trackHeight.value)
);
}
return 0;
});
const trackWidth = computed(() => {
// 横向滚动条宽度
if (props.horizontal && isXScroll.value) {
if (containerWidth.value && contentWidth.value && railWidth.value) {
const value = (railWidth.value * containerWidth.value) / contentWidth.value + 1.5 * props.size;
return Number(value.toFixed(4));
}
}
return 0;
});
const trackLeft = computed(() => {
// 滚动条水平偏移
if (containerWidth.value && contentWidth.value && railWidth.value) {
return (
(containerScrollLeft.value / (contentWidth.value - containerWidth.value)) * (railWidth.value - trackWidth.value)
);
}
return 0;
});
useEventListener(window, 'resize', updateState);
const options = {childList: true, attributes: true, subtree: true};
useMutationObserver(scrollbarRef, updateState, options);
const debounceHideEvent = debounce(hideScrollbar, props.delay);
onMounted(() => {
updateState();
});
function hideScrollbar() {
if (!trackHover.value) {
showTrack.value = false;
}
}
function updateScrollState() {
containerScrollTop.value = containerRef.value.scrollTop;
containerScrollLeft.value = containerRef.value.scrollLeft;
}
function updateScrollbarState() {
containerScrollHeight.value = containerRef.value.scrollHeight;
containerScrollWidth.value = containerRef.value.scrollWidth;
containerClientHeight.value = containerRef.value.clientHeight;
containerClientWidth.value = containerRef.value.clientWidth;
containerHeight.value = containerRef.value.offsetHeight;
containerWidth.value = containerRef.value.offsetWidth;
contentHeight.value = contentRef.value.offsetHeight;
contentWidth.value = contentRef.value.offsetWidth;
railHeight.value = railVerticalRef.value.offsetHeight;
railWidth.value = railHorizontalRef.value.offsetWidth;
}
function updateState() {
updateScrollState();
updateScrollbarState();
}
function onScroll(e: Event) {
if (autoShowTrack.value) {
showTrack.value = true;
if (!trackXPressed.value && !trackYPressed.value) {
debounceHideEvent();
}
}
emit('scroll', e);
updateScrollState();
}
function onMouseEnter() {
if (trackXPressed.value || trackYPressed.value) {
mouseLeave.value = false;
} else {
if (!autoShowTrack.value) {
showTrack.value = true;
}
}
}
function onMouseLeave() {
if (trackXPressed.value || trackYPressed.value) {
mouseLeave.value = true;
} else {
if (!autoShowTrack.value) {
showTrack.value = false;
}
}
}
function onEnterTrack() {
trackHover.value = true;
}
function onLeaveTrack() {
if (trackXPressed.value || trackYPressed.value) {
trackLeave.value = true;
} else {
trackHover.value = false;
debounceHideEvent();
}
}
function onTrackVerticalMouseDown(e: MouseEvent) {
trackYPressed.value = true;
memoYTop.value = containerScrollTop.value;
memoMouseY.value = e.clientY;
window.onmousemove = (e: MouseEvent) => {
const diffY = e.clientY - memoMouseY.value;
const dScrollTop =
(diffY * (contentHeight.value - containerHeight.value)) / (containerHeight.value - trackHeight.value);
const toScrollTopUpperBound = contentHeight.value - containerHeight.value;
let toScrollTop = memoYTop.value + dScrollTop;
toScrollTop = Math.min(toScrollTopUpperBound, toScrollTop);
toScrollTop = Math.max(toScrollTop, 0);
containerRef.value.scrollTop = toScrollTop;
};
window.onmouseup = () => {
window.onmousemove = null;
trackYPressed.value = false;
if (props.trigger === 'hover' && mouseLeave.value) {
showTrack.value = false;
mouseLeave.value = false;
}
if (autoShowTrack.value && trackLeave.value) {
trackLeave.value = false;
trackHover.value = false;
debounceHideEvent();
}
};
}
function onTrackHorizontalMouseDown(e: MouseEvent) {
trackXPressed.value = true;
memoXLeft.value = containerScrollLeft.value;
memoMouseX.value = e.clientX;
window.onmousemove = (e: MouseEvent) => {
const diffX = e.clientX - memoMouseX.value;
const dScrollLeft =
(diffX * (contentWidth.value - containerWidth.value)) / (containerWidth.value - trackWidth.value);
const toScrollLeftUpperBound = contentWidth.value - containerWidth.value;
let toScrollLeft = memoXLeft.value + dScrollLeft;
toScrollLeft = Math.min(toScrollLeftUpperBound, toScrollLeft);
toScrollLeft = Math.max(toScrollLeft, 0);
containerRef.value.scrollLeft = toScrollLeft;
};
window.onmouseup = () => {
window.onmousemove = null;
trackXPressed.value = false;
if (props.trigger === 'hover' && mouseLeave.value) {
showTrack.value = false;
mouseLeave.value = false;
}
if (autoShowTrack.value && trackLeave.value) {
trackLeave.value = false;
trackHover.value = false;
debounceHideEvent();
}
};
}
function scrollTo(...args: any[]) {
containerRef.value?.scrollTo(...args);
}
function scrollBy(...args: any[]) {
containerRef.value?.scrollBy(...args);
}
defineExpose({
scrollTo,
scrollBy
});
</script>
<template>
<div
ref="scrollbarRef"
class="m-scrollbar"
:style="`--scrollbar-size: ${size}px;`"
@mouseenter="isScroll && trigger === 'hover' ? onMouseEnter() : () => false"
@mouseleave="isScroll && trigger === 'hover' ? onMouseLeave() : () => false"
>
<div ref="containerRef" class="scrollbar-container" @scroll="onScroll">
<div
ref="contentRef"
class="scrollbar-content"
:class="contentClass"
:style="[horizontal ? { ...horizontalContentStyle, ...contentStyle } : contentStyle]"
>
<slot></slot>
</div>
</div>
<div ref="railVerticalRef" class="scrollbar-rail rail-vertical">
<div
class="scrollbar-track"
:class="{ 'track-visible': trigger === 'none' || showTrack }"
:style="`top: ${trackTop}px; height: ${trackHeight}px;`"
@mouseenter="autoShowTrack ? onEnterTrack() : () => false"
@mouseleave="autoShowTrack ? onLeaveTrack() : () => false"
@mousedown.prevent.stop="onTrackVerticalMouseDown"
></div>
</div>
<div ref="railHorizontalRef" v-show="horizontal" class="scrollbar-rail rail-horizontal">
<div
class="scrollbar-track"
:class="{ 'track-visible': trigger === 'none' || showTrack }"
:style="`left: ${trackLeft}px; width: ${trackWidth}px;`"
@mouseenter="autoShowTrack ? onEnterTrack() : () => false"
@mouseleave="autoShowTrack ? onLeaveTrack() : () => false"
@mousedown.prevent.stop="onTrackHorizontalMouseDown"
></div>
</div>
</div>
</template>
<style lang="less" scoped>
.m-scrollbar {
overflow: hidden;
position: relative;
z-index: auto;
height: 100%;
width: 100%;
.scrollbar-container {
width: 100%;
overflow: scroll;
height: 100%;
min-height: inherit;
max-height: inherit;
scrollbar-width: none;
&::-webkit-scrollbar,
&::-webkit-scrollbar-track-piece,
&::-webkit-scrollbar-thumb {
width: 0;
height: 0;
display: none;
}
.scrollbar-content {
box-sizing: border-box;
min-width: 100%;
}
}
.scrollbar-rail {
position: absolute;
pointer-events: none;
user-select: none;
background: transparent;
-webkit-user-select: none;
.scrollbar-track {
z-index: 1;
position: absolute;
cursor: pointer;
opacity: 0;
pointer-events: none;
background-color: rgba(0, 0, 0, 0.25);
transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
background-color: rgba(0, 0, 0, 0.4);
}
}
.track-visible {
opacity: 1;
pointer-events: all;
}
}
.rail-vertical {
inset: 2px 4px 2px auto;
width: var(--scrollbar-size);
.scrollbar-track {
width: var(--scrollbar-size);
border-radius: var(--scrollbar-size);
bottom: 0;
}
}
.rail-horizontal {
inset: auto 2px 4px 2px;
height: var(--scrollbar-size);
.scrollbar-track {
height: var(--scrollbar-size);
border-radius: var(--scrollbar-size);
right: 0;
}
}
}
</style>

View File

@@ -0,0 +1,709 @@
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
spinning?: boolean // 是否为加载中状态
size?: 'small' | 'middle' | 'large' // 加载中尺寸
tip?: string // 描述文案
indicator?: 'dot' | 'spin-dot' | 'spin-line' | 'ring-circle' | 'ring-rail' | 'dynamic-circle' | 'magic-ring' // 加载指示符
color?: string // 指示符颜色,当 indicator: 'magic-ring' 时为外环颜色
spinCircleWidth?: number // 圆环宽度,单位是加载指示符宽度的百分比,仅当 indicator: 'ring-circle' | 'ring-rail' 时生效
spinCirclePercent?: number // 圆环长度百分比 (0100),单位是圆环周长的百分比,仅当 indicator: 'ring-circle' | 'ring-rail' 时生效
ringRailColor?: string // 圆环轨道颜色,仅当 indicator: 'ring-rail' 时生效
magicRingColor?: string // 内环颜色,仅当 indicator: 'magic-ring' 时生效
rotate?: boolean // spin-dot 或 spin-line 初始是否旋转,仅当 indicator: 'spin-dot' | 'spin-line' 时生效
speed?: number // spin-dot 或 spin-line 渐变旋转的动画速度,单位 ms仅当 indicator: 'spin-dot' | 'spin-line' 时生效
}
const props = withDefaults(defineProps<Props>(), {
spinning: true,
size: 'middle',
tip: undefined,
indicator: 'dot',
color: '#1677ff',
spinCircleWidth: 12,
spinCirclePercent: 33,
ringRailColor: 'rgba(0, 0, 0, 0.12)',
magicRingColor: '#4096ff',
rotate: false,
speed: 800
});
const circlePerimeter = computed(() => {
// 圆环周长
return (100 - props.spinCircleWidth) * Math.PI;
});
const circlePath = computed(() => {
// 圆环轨道路径指令
const long = 100 - props.spinCircleWidth;
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}`;
});
</script>
<template>
<div
:class="`m-spin-wrap spin-${size}`"
:style="`--color: ${color}; --magic-ring-color: ${magicRingColor}; --spin-circle-width: ${spinCircleWidth}; --speed: ${speed}ms;`"
>
<div class="m-spin" v-show="spinning">
<div class="m-spin-box">
<div v-if="indicator === 'dot'" class="m-loading-dot">
<span class="dot-item"></span>
<span class="dot-item"></span>
<span class="dot-item"></span>
<span class="dot-item"></span>
</div>
<div v-if="indicator === 'spin-dot'" class="spin-wrap-box" :class="{ 'spin-box-rotate': rotate }">
<div class="m-spin-dot">
<span class="spin-item"></span>
<span class="spin-item"></span>
<span class="spin-item"></span>
<span class="spin-item"></span>
</div>
<div class="m-spin-dot spin-rotate" :class="{ 'has-tip': tip }">
<span class="spin-item"></span>
<span class="spin-item"></span>
<span class="spin-item"></span>
<span class="spin-item"></span>
</div>
</div>
<div v-if="indicator === 'spin-line'" class="spin-wrap-box" :class="{ 'spin-box-rotate': rotate }">
<div class="m-spin-line">
<span class="spin-item"></span>
<span class="spin-item"></span>
<span class="spin-item"></span>
<span class="spin-item"></span>
</div>
<div class="m-spin-line spin-rotate" :class="{ 'has-tip': tip }">
<span class="spin-item"></span>
<span class="spin-item"></span>
<span class="spin-item"></span>
<span class="spin-item"></span>
</div>
</div>
<div v-if="indicator === 'ring-circle'" class="m-ring-circle">
<svg class="circle" viewBox="0 0 100 100">
<path
:d="circlePath"
stroke-linecap="round"
class="path"
:style="`stroke-dasharray: ${(spinCirclePercent / 100) * circlePerimeter}px, ${circlePerimeter}px;`"
fill-opacity="0"
></path>
</svg>
</div>
<div v-if="indicator === 'ring-rail'" class="m-ring-rail">
<svg class="circle" viewBox="0 0 100 100">
<path
:d="circlePath"
:stroke="ringRailColor"
stroke-linecap="round"
class="trail"
:style="`stroke-dasharray: ${circlePerimeter}px, ${circlePerimeter}px;`"
fill-opacity="0"
></path>
<path
:d="circlePath"
stroke-linecap="round"
class="path"
:style="`stroke-dasharray: ${(spinCirclePercent / 100) * circlePerimeter}px, ${circlePerimeter}px;`"
fill-opacity="0"
></path>
</svg>
</div>
<div v-if="indicator === 'dynamic-circle'" class="m-dynamic-circle">
<svg class="circle" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
</svg>
</div>
<div v-if="indicator === 'magic-ring'" class="m-magic-ring">
<div class="outer-ring"></div>
<div class="inner-ring"></div>
</div>
<p v-if="tip" class="spin-tip" :class="{ 'dot-tip': ['dot', 'spin-dot'].includes(indicator) }">{{ tip }}</p>
</div>
</div>
<div class="spin-content" :class="{ 'spin-blur': spinning }">
<slot></slot>
</div>
</div>
</template>
<style lang="less" scoped>
.m-spin-wrap {
position: relative;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.m-spin {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 9;
.m-spin-box {
text-align: center;
line-height: 0;
.m-loading-dot {
position: relative;
display: inline-block;
transform: rotate(45deg);
animation: loading-dot 1.2s linear infinite;
-webkit-animation: loading-dot 1.2s linear infinite;
@keyframes loading-dot {
100% {
transform: rotate(405deg);
}
}
.dot-item {
// 单个圆点样式
position: absolute;
background: var(--color);
border-radius: 50%;
opacity: 0.3;
animation: loading-dot-color 1s linear infinite alternate;
-webkit-animation: loading-dot-color 1s linear infinite alternate;
@keyframes loading-dot-color {
100% {
opacity: 1;
}
}
}
.dot-item:first-child {
top: 0;
left: 0;
}
.dot-item:nth-child(2) {
top: 0;
right: 0;
animation-delay: 0.4s;
-webkit-animation-delay: 0.4s;
}
.dot-item:nth-child(3) {
bottom: 0;
right: 0;
animation-delay: 0.8s;
-webkit-animation-delay: 0.8s;
}
.dot-item:last-child {
bottom: 0;
left: 0;
animation-delay: 1.2s;
-webkit-animation-delay: 1.2s;
}
}
.spin-box-rotate {
animation: spin-circle 2.4s ease-in-out;
-webkit-animation: spin-circle 2.4s ease-in-out;
}
.spin-wrap-box {
text-align: center;
line-height: 0;
position: relative;
.m-spin-dot {
position: relative;
display: inline-block;
.spin-item {
position: absolute;
background: var(--color);
border-radius: 50%;
}
.spin-item:first-child {
top: 0;
left: 0;
opacity: 0.3;
animation: spin-color-1 var(--speed) linear infinite;
-webkit-animation: spin-color-1 var(--speed) linear infinite;
}
.spin-item:nth-child(2) {
top: 0;
right: 0;
opacity: 0.5;
animation: spin-color-3 var(--speed) linear infinite;
-webkit-animation: spin-color-3 var(--speed) linear infinite;
}
.spin-item:nth-child(3) {
bottom: 0;
right: 0;
opacity: 0.7;
animation: spin-color-5 var(--speed) linear infinite;
-webkit-animation: spin-color-5 var(--speed) linear infinite;
}
.spin-item:last-child {
bottom: 0;
left: 0;
opacity: 0.9;
animation: spin-color-7 var(--speed) linear infinite;
-webkit-animation: spin-color-7 var(--speed) linear infinite;
}
}
.m-spin-line {
position: relative;
display: inline-block;
.spin-item {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
background-color: var(--color);
}
.spin-item:first-child {
opacity: 0.3;
animation: spin-color-1 var(--speed) linear infinite;
-webkit-animation: spin-color-1 var(--speed) linear infinite;
}
.spin-item:nth-child(2) {
opacity: 0.5;
transform: translateX(-50%) rotate(90deg);
animation: spin-color-3 var(--speed) linear infinite;
-webkit-animation: spin-color-3 var(--speed) linear infinite;
}
.spin-item:nth-child(3) {
opacity: 0.7;
transform: translateX(-50%) rotate(180deg);
animation: spin-color-5 var(--speed) linear infinite;
-webkit-animation: spin-color-5 var(--speed) linear infinite;
}
.spin-item:last-child {
opacity: 0.9;
transform: translateX(-50%) rotate(270deg);
animation: spin-color-7 var(--speed) linear infinite;
-webkit-animation: spin-color-7 var(--speed) linear infinite;
}
}
.spin-rotate {
position: absolute;
left: 0;
transform: rotate(45deg);
.spin-item:first-child {
opacity: 0.4;
animation: spin-color-2 var(--speed) linear infinite;
-webkit-animation: spin-color-2 var(--speed) linear infinite;
}
.spin-item:nth-child(2) {
opacity: 0.6;
animation: spin-color-4 var(--speed) linear infinite;
-webkit-animation: spin-color-4 var(--speed) linear infinite;
}
.spin-item:nth-child(3) {
opacity: 0.8;
animation: spin-color-6 var(--speed) linear infinite;
-webkit-animation: spin-color-6 var(--speed) linear infinite;
}
.spin-item:last-child {
opacity: 1;
animation: spin-color-8 var(--speed) linear infinite;
-webkit-animation: spin-color-8 var(--speed) linear infinite;
}
}
.has-tip {
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
@keyframes spin-color-1 {
0% {
opacity: 0.3;
}
14.3% {
opacity: 1;
}
100% {
opacity: 0.4;
}
}
@keyframes spin-color-2 {
0% {
opacity: 0.4;
}
14.3% {
opacity: 0.3;
}
28.6% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}
@keyframes spin-color-3 {
0% {
opacity: 0.5;
}
28.6% {
opacity: 0.3;
}
42.8% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}
@keyframes spin-color-4 {
0% {
opacity: 0.6;
}
42.8% {
opacity: 0.3;
}
57.1% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
@keyframes spin-color-5 {
0% {
opacity: 0.7;
}
57.1% {
opacity: 0.3;
}
71.4% {
opacity: 1;
}
100% {
opacity: 0.8;
}
}
@keyframes spin-color-6 {
0% {
opacity: 0.8;
}
71.4% {
opacity: 0.3;
}
85.7% {
opacity: 1;
}
100% {
opacity: 0.9;
}
}
@keyframes spin-color-7 {
0% {
opacity: 0.9;
}
85.7% {
opacity: 0.3;
}
100% {
opacity: 1;
}
}
@keyframes spin-color-8 {
0% {
opacity: 1;
}
100% {
opacity: 0.3;
}
}
}
.m-ring-circle,
.m-ring-rail {
display: inline-block;
overflow: hidden;
animation: spin-circle 0.8s linear infinite;
-webkit-animation: spin-circle 0.8s linear infinite;
}
.circle {
.trail {
stroke-width: var(--spin-circle-width);
stroke-dashoffset: 0;
}
.path {
stroke: var(--color);
stroke-width: var(--spin-circle-width);
stroke-dashoffset: 0;
}
}
.m-dynamic-circle {
display: inline-block;
animation: spin-circle 2s linear infinite;
-webkit-animation: spin-circle 2s linear infinite;
.circle {
.path {
stroke-width: 5;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke: var(--color);
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);
}
}
.m-magic-ring {
display: inline-block;
position: relative;
transform: rotate(45deg);
animation: spin-rotate 2.5s linear infinite;
-webkit-animation: spin-rotate 2.5s linear infinite;
@keyframes spin-rotate {
100% {
transform: rotate(405deg);
}
}
.outer-ring {
position: absolute;
width: 100%;
height: 100%;
border-style: solid;
border-color: var(--color);
border-radius: 50%;
animation: spin-outer-ring 1.5s linear infinite;
-webkit-animation: spin-outer-ring 1.5s linear infinite;
@keyframes spin-outer-ring {
100% {
transform: rotateY(360deg);
}
}
}
.inner-ring {
position: absolute;
border-style: solid;
border-color: var(--magic-ring-color);
border-radius: 50%;
animation: spin-inner-ring 1.5s linear infinite;
-webkit-animation: spin-inner-ring 1.5s linear infinite;
@keyframes spin-inner-ring {
0% {
transform: rotateY(45deg);
}
100% {
transform: rotateY(405deg);
}
}
}
}
.spin-tip {
color: var(--color);
text-align: center;
}
}
}
.spin-content {
width: 100%;
height: 100%;
transition: opacity 0.3s;
}
.spin-blur {
opacity: 0.5;
user-select: none;
pointer-events: none;
}
}
.spin-small {
.m-spin .m-spin-box {
.m-loading-dot {
width: 20px;
height: 20px;
.dot-item {
width: 8px;
height: 8px;
}
}
.m-spin-dot {
width: 20px;
height: 20px;
.spin-item {
width: 6px;
height: 6px;
}
}
.m-spin-line {
--line-length: 8px;
width: calc(var(--line-length) * 3);
height: calc(var(--line-length) * 3);
.spin-item {
transform-origin: 50% calc(var(--line-length) * 1.5);
border-radius: var(--line-length);
width: calc(var(--line-length) / 2.5);
height: var(--line-length);
}
}
.m-ring-circle,
.m-ring-rail {
width: 24px;
height: 24px;
}
.m-dynamic-circle {
width: 26px;
height: 26px;
}
.m-magic-ring {
width: 24px;
height: 24px;
.outer-ring,
.inner-ring {
border-width: 3px;
}
.inner-ring {
top: 3px;
left: 3px;
width: calc(100% - 6px);
height: calc(100% - 6px);
}
}
.spin-tip {
font-size: 14px;
font-weight: 400;
line-height: 16px;
margin-top: 8px;
}
.dot-tip {
margin-top: 12px;
}
}
}
.spin-middle {
.m-spin .m-spin-box {
.m-loading-dot {
width: 30px;
height: 30px;
.dot-item {
width: 11px;
height: 11px;
}
}
.m-spin-dot {
width: 30px;
height: 30px;
.spin-item {
width: 9px;
height: 9px;
}
}
.m-spin-line {
--line-length: 12px;
width: calc(var(--line-length) * 3);
height: calc(var(--line-length) * 3);
.spin-item {
transform-origin: 50% calc(var(--line-length) * 1.5);
border-radius: var(--line-length);
width: calc(var(--line-length) / 3);
height: var(--line-length);
}
}
.m-ring-circle,
.m-ring-rail {
width: 36px;
height: 36px;
}
.m-dynamic-circle {
width: 38px;
height: 38px;
}
.m-magic-ring {
width: 36px;
height: 36px;
.outer-ring,
.inner-ring {
border-width: 5px;
}
.inner-ring {
top: 5px;
left: 5px;
width: calc(100% - 10px);
height: calc(100% - 10px);
}
}
.spin-tip {
font-size: 14px;
font-weight: 500;
line-height: 18px;
margin-top: 12px;
}
.dot-tip {
margin-top: 16px;
}
}
}
.spin-large {
.m-spin .m-spin-box {
.m-loading-dot {
width: 40px;
height: 40px;
.dot-item {
width: 15px;
height: 15px;
}
}
.m-spin-dot {
width: 40px;
height: 40px;
.spin-item {
width: 12px;
height: 12px;
}
}
.m-spin-line {
--line-length: 16px;
width: calc(var(--line-length) * 3);
height: calc(var(--line-length) * 3);
.spin-item {
transform-origin: 50% calc(var(--line-length) * 1.5);
border-radius: var(--line-length);
width: calc(var(--line-length) / 3);
height: var(--line-length);
}
}
.m-ring-circle,
.m-ring-rail {
width: 48px;
height: 48px;
}
.m-dynamic-circle {
width: 50px;
height: 50px;
}
.m-magic-ring {
width: 48px;
height: 48px;
.outer-ring,
.inner-ring {
border-width: 6px;
}
.inner-ring {
top: 6px;
left: 6px;
width: calc(100% - 12px);
height: calc(100% - 12px);
}
}
.spin-tip {
font-size: 16px;
font-weight: 500;
line-height: 20px;
margin-top: 16px;
}
.dot-tip {
margin-top: 22px;
}
}
}
</style>

View File

@@ -0,0 +1,279 @@
<script setup lang="ts">
import {computed, nextTick, onMounted, ref, watch} from 'vue';
interface Props {
width?: string | number // 文本域宽度,单位 px
allowClear?: boolean // 可以点击清除图标删除内容
autoSize?: boolean | { minRows?: number; maxRows?: number } // 自适应内容高度
disabled?: boolean // 是否禁用
placeholder?: string // 文本域输入的占位符
maxlength?: number // 文字最大长度
showCount?: boolean // 是否展示字数
value?: string // (v-model) 文本域内容
valueModifiers?: object // 用于访问组件的 v-model 上添加的修饰符
}
const props = withDefaults(defineProps<Props>(), {
width: '100%',
allowClear: false,
autoSize: false,
disabled: false,
placeholder: undefined,
maxlength: undefined,
showCount: false,
value: '',
valueModifiers: () => ({})
});
const textareaRef = ref();
const areaHeight = ref(32);
const textareaWidth = computed(() => {
if (typeof props.width === 'number') {
return `${props.width}px`;
}
return props.width;
});
const autoSizeStyle = computed(() => {
if (typeof props.autoSize === 'object') {
const style: { 'min-height'?: string; 'max-height'?: string; [propName: string]: any } = {
height: `${areaHeight.value}px`,
resize: 'none'
};
if ('minRows' in props.autoSize) {
style['min-height'] = (props.autoSize.minRows as number) * 22 + 10 + 'px';
}
if ('maxRows' in props.autoSize) {
style['max-height'] = (props.autoSize.maxRows as number) * 22 + 10 + 'px';
}
return style;
}
if (typeof props.autoSize === 'boolean') {
if (props.autoSize) {
return {
height: `${areaHeight.value}px`,
resize: 'none'
};
}
return {};
}
return {};
});
const showClear = computed(() => {
return !props.disabled && props.allowClear && props.value;
});
const showCountNum = computed(() => {
if (props.maxlength) {
return `${props.value.length} / ${props.maxlength}`;
}
return props.value.length;
});
const lazyTextarea = computed(() => {
return 'lazy' in props.valueModifiers;
});
watch(
() => props.value,
async () => {
if (JSON.stringify(autoSizeStyle.value) !== '{}') {
areaHeight.value = 32;
await nextTick();
getAreaHeight();
}
},
{
flush: 'post'
}
);
onMounted(() => {
getAreaHeight();
});
function parseEmojis(text: string): string {
const regex = /\[((1[0-6][0-6]|[1-9]?[0-9])\.gif)]/g; // 匹配 [1.gif] 的字符串
return text.replace(regex, (_match, p1) => {
return `<img width="30px" height="30px" loading="lazy" src="/emoji/qq/gif/${p1}" alt="emoji ${p1}" />`;
});
}
const renderedContent = computed(() => parseEmojis(props.value));
function getAreaHeight() {
areaHeight.value = textareaRef.value.scrollHeight + 2;
}
const emits = defineEmits(['update:value', 'change', 'enter']);
function onInput(e: Event) {
if (!lazyTextarea.value) {
emits('update:value', (e.target as HTMLInputElement).value);
emits('change', e);
}
}
function onChange(e: Event) {
if (lazyTextarea.value) {
emits('update:value', (e.target as HTMLInputElement).value);
emits('change', e);
}
}
async function onKeyboard(e: KeyboardEvent) {
emits('enter', e);
if (lazyTextarea.value) {
textareaRef.value.blur();
await nextTick();
textareaRef.value.focus();
}
}
function onClear() {
emits('update:value', '');
textareaRef.value.focus();
}
</script>
<template>
<div
class="m-textarea"
:class="{ 'show-count': showCount }"
:style="`width: ${textareaWidth};`"
:data-count="showCountNum"
>
<textarea
ref="textareaRef"
type="hidden"
class="u-textarea"
:class="{ 'clear-class': showClear, 'textarea-disabled': disabled }"
:style="autoSizeStyle"
:value="value"
:placeholder="placeholder"
:maxlength="maxlength"
:disabled="disabled"
@input="onInput"
@change="onChange"
@keydown.enter="onKeyboard"
/>
<div v-html="renderedContent" class="rendered-content"></div>
<svg
v-if="showClear"
class="clear-svg"
@click="onClear"
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>
</div>
</template>
<style lang="less" scoped>
.m-textarea {
position: relative;
display: inline-block;
.u-textarea {
width: 100%;
min-width: 0;
min-height: 32px;
max-width: 100%;
height: auto;
padding: 4px 11px;
color: rgba(0, 0, 0, 0.88);
font-size: 14px;
line-height: 1.5714285714285714;
list-style: none;
transition: all 0.3s,
height 0s;
resize: vertical;
position: relative;
z-index: 9;
display: inline-block;
vertical-align: bottom;
text-overflow: ellipsis;
background-color: #ffffff;
border: 1px solid #d9d9d9;
border-radius: 6px;
outline: none;
&:hover {
border-color: #4096ff;
z-index: 1;
}
&:focus-within {
border-color: #4096ff;
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
outline: 0;
}
}
.clear-class {
padding-right: 24px;
}
textarea:disabled {
color: rgba(0, 0, 0, 0.25);
}
textarea::-webkit-input-placeholder {
color: rgba(0, 0, 0, 0.25);
}
textarea:-moz-placeholder {
color: rgba(0, 0, 0, 0.25);
}
textarea::-moz-placeholder {
color: rgba(0, 0, 0, 0.25);
}
textarea:-ms-input-placeholder {
color: rgba(0, 0, 0, 0.25);
}
.clear-svg {
position: absolute;
top: 9px;
right: 8px;
z-index: 99;
display: inline-block;
font-size: 12px;
color: rgba(0, 0, 0, 0.25);
fill: currentColor;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: rgba(0, 0, 0, 0.45);
}
}
.textarea-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;
}
}
}
.show-count {
&::after {
color: rgba(0, 0, 0, 0.45);
white-space: nowrap;
content: attr(data-count);
pointer-events: none;
float: right;
}
}
</style>

View File

@@ -0,0 +1,573 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue';
import {cancelRaf, rafTimeout, useEventListener, useSlotsExist} from '../utils';
interface Props {
maxWidth?: string | number // 弹出提示最大宽度,单位 px
content?: string // 展示的内容 string | slot
contentStyle?: CSSProperties // 设置展示内容的样式
tooltip?: string // 弹出提示内容 string | slot
tooltipClass?: string // 设置弹出提示的类名
tooltipStyle?: CSSProperties // 设置弹出提示的样式
bgColor?: string // 弹出提示框背景颜色
arrow?: boolean // 是否显示箭头
placement?: 'top' | 'bottom' | 'left' | 'right' // 弹出提示位置
flip?: boolean // 弹出提示被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置
trigger?: 'hover' | 'click' // 弹出提示触发方式
keyboard?: boolean // 是否支持按键操作 (enter 显示esc 关闭),仅当 trigger: 'click' 时生效
transitionDuration?: number // 弹出提示动画的过渡持续时间,单位 ms
showDelay?: number // 弹出提示显示的延迟时间,单位 ms
hideDelay?: number // 弹出提示隐藏的延迟时间,单位 ms
show?: boolean // (v-model) 弹出提示是否显示
}
const props = withDefaults(defineProps<Props>(), {
maxWidth: 240,
content: undefined,
contentStyle: () => ({}),
tooltip: undefined,
tooltipClass: undefined,
tooltipStyle: () => ({}),
bgColor: 'rgba(0, 0, 0, 0.85)',
arrow: true,
placement: 'top',
flip: true,
trigger: 'hover',
keyboard: false,
transitionDuration: 100,
showDelay: 100,
hideDelay: 100,
show: false
});
const tooltipVisible = ref<boolean>(false);
const hideTimer = ref();
const scrollTarget = ref<HTMLElement | null>(null); // 最近的可滚动父元素
const contentRect = ref(); // content 的矩形信息
const top = ref(0); // 提示框 top 定位
const left = ref(0); // 提示框 left 定位
const tooltipPlace = ref('top'); // 弹出提示位置
const contentRef = ref(); // 声明一个同名的模板引用
const contentWidth = ref(); // 展示内容宽度
const contentHeight = ref(); // 展示内容高度
const tooltipRef = ref(); // 声明一个同名的模板引用
const tooltipWidth = ref(); // 弹出提示内容宽度
const tooltipHeight = ref(); // 弹出提示内容高度
const activeBlur = ref(false); // 是否激活 blur 事件
const emits = defineEmits(['update:show', 'openChange']);
const slotsExist = useSlotsExist(['tooltip']);
const viewportWidth = ref(document.documentElement.clientWidth); // 视口宽度(不包括滚动条)
const viewportHeight = ref(document.documentElement.clientHeight); // 视口高度(不包括滚动条)
const tooltipMaxWidth = computed(() => {
if (typeof props.maxWidth === 'number') {
return `${props.maxWidth}px`;
}
return props.maxWidth;
});
const showTooltip = computed(() => {
return slotsExist.tooltip || props.tooltip;
});
const tooltipPlacement = computed(() => {
switch (tooltipPlace.value) {
case 'top':
return {
transformOrigin: `50% ${top.value}px`,
top: `${-top.value}px`,
left: `${-left.value}px`
};
case 'bottom':
return {
transformOrigin: `50% ${props.arrow ? -4 : -6}px`,
bottom: `${-top.value}px`,
left: `${-left.value}px`
};
case 'left':
return {
transformOrigin: `${left.value}px 50%`,
top: `${-top.value}px`,
left: `${-left.value}px`
};
case 'right':
return {
transformOrigin: `${props.arrow ? -4 : -6}px 50%`,
top: `${-top.value}px`,
right: `${-left.value}px`
};
default:
return {
transformOrigin: `50% ${top.value}px`,
top: `${-top.value}px`,
left: `${-left.value}px`
};
}
});
watch(
() => props.show,
(to) => {
tooltipVisible.value = to;
},
{
immediate: true
}
);
watch(
() => [tooltipMaxWidth.value, props.placement, props.arrow, props.flip],
() => {
getPosition();
},
{
deep: true,
flush: 'post'
}
);
onMounted(() => {
observeScroll();
});
onBeforeUnmount(() => {
cleanup();
});
useEventListener(window, 'resize', getViewportSize);
function getViewportSize() {
viewportWidth.value = document.documentElement.clientWidth;
viewportHeight.value = document.documentElement.clientHeight;
getPosition();
}
function observeScroll() {
// 监听可滚动父元素
cleanup();
scrollTarget.value = getScrollParent(contentRef.value?.parentElement ?? null);
if (scrollTarget.value) {
scrollTarget.value.addEventListener('scroll', getPosition);
}
}
function cleanup() {
if (scrollTarget.value) {
scrollTarget.value.removeEventListener('scroll', getPosition);
}
scrollTarget.value = null;
}
function getScrollParent(el: HTMLElement | null): HTMLElement | null {
const isScrollable = (el: HTMLElement): boolean => {
const style = window.getComputedStyle(el);
if (
(el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth) &&
(style.overflow === 'scroll' || style.overflow === 'auto')
) {
return true;
}
return false;
};
if (el) {
return isScrollable(el) ? el : getScrollParent(el.parentElement ?? null);
}
return null;
}
function getPosition() {
contentWidth.value = contentRef.value.offsetWidth;
contentHeight.value = contentRef.value.offsetHeight;
tooltipWidth.value = tooltipRef.value.offsetWidth;
tooltipHeight.value = tooltipRef.value.offsetHeight;
if (props.flip) {
contentRect.value = contentRef.value.getBoundingClientRect();
tooltipPlace.value = getPlacement(props.placement, []);
}
if (['top', 'bottom'].includes(tooltipPlace.value)) {
top.value = tooltipHeight.value + (props.arrow ? 4 : 6);
left.value = (tooltipWidth.value - contentWidth.value) / 2;
} else {
top.value = (tooltipHeight.value - contentHeight.value) / 2;
left.value = tooltipWidth.value + (props.arrow ? 4 : 6);
}
}
// 获取可滚动父元素或视口的矩形信息
function getShelterRect() {
if (scrollTarget.value) {
return scrollTarget.value.getBoundingClientRect();
} else {
return {
top: 0,
left: 0,
bottom: viewportHeight.value,
right: viewportWidth.value
};
}
}
// 弹出提示被浏览器窗口或最近可滚动父元素遮挡时自动调整弹出位置
function getPlacement(place: string, disabledPlaces: string[]): any {
const {top, bottom, left, right} = contentRect.value; // 内容元素各边缘相对于浏览器视口的位置(不包括滚动条)
const {top: targetTop, bottom: targetBottom, left: targetLeft, right: targetRight} = getShelterRect(); // 滚动元素或视口各边缘相对于浏览器视口的位置(不包括滚动条)
const topDistance = top - targetTop; // 内容元素上边缘距离滚动元素上边缘的距离
const bottomDistance = targetBottom - bottom; // 内容元素下边缘距离动元素下边缘的距离
const leftDistance = left - targetLeft; // 内容元素左边缘距离滚动元素左边缘的距离
const rightDistance = targetRight - right; // 内容元素右边缘距离滚动元素右边缘的距离
const horizontalDistance = (tooltipWidth.value - contentWidth.value) / 2; // 水平方向容纳弹出提示需要的最小宽度
const verticalDistance = (tooltipHeight.value - contentHeight.value) / 2; // 垂直方向容纳弹出提示需要的最小高度
switch (place) {
case 'top':
if (!disabledPlaces.includes('top')) {
if (topDistance < tooltipHeight.value + (props.arrow ? 4 : 6) && disabledPlaces.length !== 3) {
return getPlacement('bottom', [...disabledPlaces, 'top']);
} else {
if (leftDistance >= horizontalDistance && rightDistance >= horizontalDistance) {
return 'top';
} else {
if (disabledPlaces.length !== 3) {
if (leftDistance < horizontalDistance) {
return getPlacement('right', [...disabledPlaces, 'top', 'bottom', 'left']);
}
if (rightDistance < horizontalDistance) {
return getPlacement('left', [...disabledPlaces, 'top', 'bottom', 'right']);
}
}
}
}
} else {
if (!disabledPlaces.includes('bottom')) {
return getPlacement('bottom', [...disabledPlaces, 'top']);
}
if (!disabledPlaces.includes('left')) {
return getPlacement('left', [...disabledPlaces, 'top']);
}
}
break;
case 'bottom':
if (!disabledPlaces.includes('bottom')) {
if (bottomDistance < tooltipHeight.value + (props.arrow ? 4 : 6) && disabledPlaces.length !== 3) {
return getPlacement('top', [...disabledPlaces, 'bottom']);
} else {
if (leftDistance >= horizontalDistance && rightDistance >= horizontalDistance) {
return 'bottom';
} else {
if (disabledPlaces.length !== 3) {
if (leftDistance < horizontalDistance) {
return getPlacement('right', [...disabledPlaces, 'top', 'bottom', 'left']);
}
if (rightDistance < horizontalDistance) {
return getPlacement('left', [...disabledPlaces, 'top', 'bottom', 'right']);
}
}
}
}
} else {
if (!disabledPlaces.includes('top')) {
return getPlacement('top', [...disabledPlaces, 'bottom']);
}
if (!disabledPlaces.includes('left')) {
return getPlacement('left', [...disabledPlaces, 'bottom']);
}
}
break;
case 'left':
if (!disabledPlaces.includes('left')) {
if (leftDistance < tooltipWidth.value + (props.arrow ? 4 : 6) && disabledPlaces.length !== 3) {
return getPlacement('right', [...disabledPlaces, 'left']);
} else {
if (topDistance >= verticalDistance && bottomDistance >= verticalDistance) {
return 'left';
} else {
if (disabledPlaces.length !== 3) {
if (topDistance < verticalDistance) {
return getPlacement('bottom', [...disabledPlaces, 'left', 'right', 'top']);
}
if (bottomDistance < verticalDistance) {
return getPlacement('top', [...disabledPlaces, 'left', 'right', 'bottom']);
}
}
}
}
} else {
if (!disabledPlaces.includes('right')) {
return getPlacement('right', [...disabledPlaces, 'left']);
}
if (!disabledPlaces.includes('top')) {
return getPlacement('top', [...disabledPlaces, 'left']);
}
}
if (topDistance >= verticalDistance && bottomDistance >= verticalDistance) {
return 'left';
}
break;
case 'right':
if (!disabledPlaces.includes('right')) {
if (rightDistance < tooltipWidth.value + (props.arrow ? 4 : 6) && disabledPlaces.length !== 3) {
return getPlacement('left', [...disabledPlaces, 'right']);
} else {
if (topDistance >= verticalDistance && bottomDistance >= verticalDistance) {
return 'right';
} else {
if (disabledPlaces.length !== 3) {
if (topDistance < verticalDistance) {
return getPlacement('bottom', [...disabledPlaces, 'left', 'right', 'top']);
}
if (bottomDistance < verticalDistance) {
return getPlacement('top', [...disabledPlaces, 'left', 'right', 'bottom']);
}
}
}
}
} else {
if (!disabledPlaces.includes('left')) {
return getPlacement('left', [...disabledPlaces, 'right']);
}
if (!disabledPlaces.includes('top')) {
return getPlacement('top', [...disabledPlaces, 'right']);
}
}
break;
default:
return props.placement;
}
}
function onShow() {
if (hideTimer.value) {
cancelRaf(hideTimer.value);
}
if (!tooltipVisible.value) {
getPosition();
rafTimeout(() => {
tooltipVisible.value = true;
emits('update:show', true);
emits('openChange', true);
}, props.showDelay);
}
}
function onHide(): void {
hideTimer.value = rafTimeout(() => {
tooltipVisible.value = false;
emits('update:show', false);
emits('openChange', false);
}, props.hideDelay);
}
function toggleVisible() {
if (!tooltipVisible.value) {
onShow();
} else {
onHide();
}
}
function onEnter() {
activeBlur.value = false;
}
function onLeave() {
activeBlur.value = true;
tooltipRef.value.focus();
}
defineExpose({
show: onShow,
hide: onHide
});
</script>
<template>
<div
class="m-tooltip-wrap"
:style="`--tooltip-max-width: ${tooltipMaxWidth}; --tooltip-background-color: ${bgColor}; --transition-duration: ${transitionDuration}ms;`"
@mouseenter="trigger === 'hover' ? onShow() : () => false"
@mouseleave="trigger === 'hover' ? onHide() : () => false"
>
<div
ref="tooltipRef"
tabindex="1"
class="m-tooltip-card"
:class="{
[`tooltip-${tooltipPlace}-padding`]: arrow,
'tooltip-visible': showTooltip && tooltipVisible
}"
:style="tooltipPlacement"
@blur="trigger === 'click' && activeBlur ? onHide() : () => false"
@mouseenter="trigger === 'hover' ? onShow() : () => false"
@mouseleave="trigger === 'hover' ? onHide() : () => false"
@keydown.esc="trigger === 'click' && keyboard && tooltipVisible ? onHide() : () => false"
>
<div class="tooltip-card" :class="tooltipClass" :style="tooltipStyle">
<slot name="tooltip">{{ tooltip }}</slot>
</div>
<div v-if="arrow" class="tooltip-arrow" :class="`arrow-${tooltipPlace || 'top'}`"></div>
</div>
<span
ref="contentRef"
class="tooltip-content"
:style="contentStyle"
@click="trigger === 'click' ? toggleVisible() : () => false"
@keydown.enter="trigger === 'click' && keyboard ? toggleVisible() : () => false"
@keydown.esc="trigger === 'click' && keyboard && tooltipVisible ? onHide() : () => false"
@mouseenter="trigger === 'click' && tooltipVisible ? onEnter() : () => false"
@mouseleave="trigger === 'click' && tooltipVisible ? onLeave() : () => false"
>
<slot>{{ content }}</slot>
</span>
</div>
</template>
<style lang="less" scoped>
.m-tooltip-wrap {
position: relative;
display: inline-block;
.m-tooltip-card {
position: absolute;
z-index: 999;
width: max-content;
max-width: var(--tooltip-max-width);
pointer-events: none;
outline: none;
transform: scale(0.8);
opacity: 0;
transition: opacity var(--transition-duration) cubic-bezier(0.78, 0.14, 0.15, 0.86),
transform var(--transition-duration) cubic-bezier(0.78, 0.14, 0.15, 0.86);
.tooltip-card {
min-width: 32px;
min-height: 32px;
padding: 6px 8px;
font-size: 14px;
color: #fff;
line-height: 1.5714285714285714;
text-align: justify;
text-decoration: none;
word-break: break-all;
background-color: var(--tooltip-background-color);
border-radius: 6px;
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);
}
.tooltip-arrow {
position: absolute;
z-index: 9;
display: block;
pointer-events: none;
width: 16px;
height: 16px;
overflow: hidden;
&::before {
position: absolute;
width: 16px;
height: 8px;
background-color: var(--tooltip-background-color);
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;
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: '';
}
}
.arrow-top {
left: 50%;
bottom: 12px;
transform: translateX(-50%) translateY(100%) rotate(180deg);
&::before {
bottom: 0;
left: 0;
}
&::after {
bottom: 0;
inset-inline: 0;
}
}
.arrow-bottom {
left: 50%;
top: 12px;
transform: translateX(-50%) translateY(-100%) rotate(0deg);
&::before {
bottom: 0;
left: 0;
}
&::after {
bottom: 0;
inset-inline: 0;
}
}
.arrow-left {
top: 50%;
right: 12px;
transform: translateX(100%) translateY(-50%) rotate(90deg);
&::before {
bottom: 0;
left: 0;
}
&::after {
bottom: 0;
inset-inline: 0;
}
}
.arrow-right {
top: 50%;
left: 12px;
transform: translateX(-100%) translateY(-50%) rotate(-90deg);
&::before {
bottom: 0;
left: 0;
}
&::after {
bottom: 0;
inset-inline: 0;
}
}
}
.tooltip-top-padding {
padding-bottom: 12px;
}
.tooltip-bottom-padding {
padding-top: 12px;
}
.tooltip-left-padding {
padding-right: 12px;
}
.tooltip-right-padding {
padding-left: 12px;
}
.tooltip-visible {
pointer-events: auto;
transform: scale(1);
opacity: 1;
transition: opacity var(--transition-duration) cubic-bezier(0.08, 0.82, 0.17, 1),
transform var(--transition-duration) cubic-bezier(0.08, 0.82, 0.17, 1);
}
.tooltip-content {
display: inline-block;
}
}
</style>

View File

@@ -0,0 +1,182 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import Spin from '../Spin/Spin.vue';
import { useResizeObserver } from '../utils';
/*
宽度固定图片等比例缩放使用JS获取每张图片宽度和高度结合 `relative` 和 `absolute` 定位
计算每个图片的位置 `top``left`,保证每张新的图片都追加在当前高度最小的那列末尾
*/
interface Image {
name?: string // 图片名称
src: string // 图片地址
}
interface Props {
images?: Image[] // 图片数组
columnCount?: number // 要划分的列数
columnGap?: number // 各列之间的间隙,单位 px
width?: string | number // 瀑布流区域的总宽度,单位 px
borderRadius?: number // 瀑布流区域和图片圆角,单位 px
backgroundColor?: string // 瀑布流区域背景填充色
spinProps?: object // Spin 组件属性配置,参考 Spin Props用于配置图片加载中样式
}
const props = withDefaults(defineProps<Props>(), {
images: () => [],
columnCount: 3,
columnGap: 20,
width: '100%',
borderRadius: 8,
backgroundColor: '#F2F4F8',
spinProps: () => ({})
});
const waterfallRef = ref();
const waterfallWidth = ref<number>();
const loaded = ref(Array(props.images.length).fill(false)); // 图片是否加载完成
const imageWidth = ref<number>();
const imagesProperty = ref<{ width: number; height: number; top: number; left: number }[]>([]);
const preColumnHeight = ref<number[]>(Array(props.columnCount).fill(0)); // 每列的高度
const flag = ref(0);
const totalWidth = computed(() => {
if (typeof props.width === 'number') {
return `${props.width}px`;
} else {
return props.width;
}
});
const height = computed(() => {
return Math.max(...preColumnHeight.value) + props.columnGap;
});
const len = computed(() => {
return props.images.length;
});
watch(
() => [props.images, props.columnCount, props.columnGap, props.width],
() => {
waterfallWidth.value = waterfallRef.value.offsetWidth;
preColumnHeight.value = Array(props.columnCount).fill(0);
flag.value++;
preloadImages(flag.value);
},
{
deep: true, // 强制转成深层侦听器
flush: 'post' // 在侦听器回调中访问被 Vue 更新之后的 DOM
}
);
onMounted(() => {
waterfallWidth.value = waterfallRef.value.offsetWidth;
preloadImages(flag.value);
});
function updateWatefall() {
const currentWidth = waterfallRef.value.offsetWidth;
// 窗口宽度改变时重新计算瀑布流布局
if (props.images.length && currentWidth !== waterfallWidth.value) {
waterfallWidth.value = currentWidth;
flag.value++;
preloadImages(flag.value);
}
}
useResizeObserver(waterfallRef, updateWatefall);
async function preloadImages(symbol: number) {
// 计算图片宽高和位置topleft
// 计算每列的图片宽度
imageWidth.value = ((waterfallWidth.value as number) - (props.columnCount + 1) * props.columnGap) / props.columnCount;
imagesProperty.value.splice(0);
for (let i = 0; i < len.value; i++) {
if (symbol === flag.value) {
await loadImage(props.images[i].src, i);
} else {
return false;
}
}
}
function loadImage(url: string, n: number) {
return new Promise((resolve) => {
const image = new Image();
image.src = url;
image.onload = function () {
// 图片加载完成时执行此时可通过image.width和image.height获取到图片原始宽高
const height = image.height / (image.width / (imageWidth.value as number));
imagesProperty.value[n] = {
// 存储图片宽高和位置信息
width: imageWidth.value as number,
height: height,
...getPosition(n, height)
};
resolve('load');
};
});
}
function getPosition(i: number, height: number) {
// 获取图片位置信息topleft
if (i < props.columnCount) {
preColumnHeight.value[i] = props.columnGap + height;
return {
top: props.columnGap,
left: ((imageWidth.value as number) + props.columnGap) * i + props.columnGap
};
} else {
const top = Math.min(...preColumnHeight.value);
let index = 0;
for (let n = 0; n < props.columnCount; n++) {
if (preColumnHeight.value[n] === top) {
index = n;
break;
}
}
preColumnHeight.value[index] = top + props.columnGap + height;
return {
top: top + props.columnGap,
left: ((imageWidth.value as number) + props.columnGap) * index + props.columnGap
};
}
}
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];
}
}
}
</script>
<template>
<div
ref="waterfallRef"
class="m-waterfall"
:style="`--border-radius: ${borderRadius}px; background-color: ${backgroundColor}; width: ${totalWidth}; height: ${height}px;`"
>
<Spin
class="waterfall-image"
:style="`width: ${property.width}px; height: ${property.height}px; top: ${property && property.top}px; left: ${property && property.left}px;`"
:spinning="!loaded[index]"
size="small"
indicator="dynamic-circle"
v-bind="spinProps"
v-for="(property, index) in imagesProperty"
:key="index"
>
<img class="u-image" :src="images[index].src" :alt="getImageName(images[index])" @load="onLoaded(index)" />
</Spin>
</div>
</template>
<style lang="less" scoped>
.m-waterfall {
position: relative;
border-radius: var(--border-radius);
.waterfall-image {
position: absolute;
.u-image {
width: 100%;
height: 100%;
border-radius: var(--border-radius);
display: inline-block;
vertical-align: bottom;
}
}
}
</style>

View File

@@ -1,3 +1,17 @@
import type {Ref} from 'vue';
import {
computed,
getCurrentInstance,
onBeforeUnmount,
onMounted,
onUnmounted,
reactive,
ref,
toValue,
useSlots,
watch
} from 'vue';
/**
* 组合式函数
* 监听给定名称或名称数组的插槽是否存在,支持监听单个插槽或一组插槽的存在
@@ -6,7 +20,6 @@
* @returns 如果是单个插槽名称,则返回一个计算属性,表示该插槽是否存在
* 如果是插槽名称数组,则返回一个 reactive 对象,其中的每个属性对应该插槽是否存在
*/
import { useSlots, reactive, computed } from 'vue';
export function useSlotsExist(slotsName: string | string[] = 'default') {
const slots = useSlots(); // 获取当前组件的所有插槽
// 检查特定名称的插槽是否存在且不为空
@@ -48,3 +61,287 @@ export function useSlotsExist(slotsName: string | string[] = 'default') {
return computed(() => checkSlotsExist(slotsName));
}
}
/**
* 组合式函数
* 使用 Vue 的生命周期钩子添加和移除事件监听器
*
* 该函数旨在提供一种优雅的方式来管理事件监听器,避免在组件卸载后仍保留事件监听器,
* 从而可能导致内存泄漏的问题;通过结合 Vue 的 onMounted 和 onUnmounted 钩子,
* 在组件挂载时添加事件监听器,并在组件卸载时移除它
*
* @param target 目标元素或对象;可以是 DOM 元素或其他支持 addEventListener 的对象
* @param event 要监听的事件名称
* @param callback 事件被触发时执行的回调函数
*/
export function useEventListener(target: HTMLElement | Window | Document, event: string, callback: EventListenerOrEventListenerObject): void {
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback as EventListenerOrEventListenerObject));
onUnmounted(() => target.removeEventListener(event, callback as EventListenerOrEventListenerObject));
}
/**
* 使用 requestAnimationFrame 实现的延迟 setTimeout 或间隔 setInterval 调用函数
*
* @param fn 要执行的函数
* @param delay 延迟的时间,单位为 ms默认为 0表示不延迟立即执行
* @param interval 是否间隔执行,如果为 true则在首次执行后以 delay 为间隔持续执行
* @returns 返回一个对象,包含一个 id 属性,该 id 为 requestAnimationFrame 的调用 ID可用于取消动画帧
*/
export function rafTimeout(fn: () => void, delay: number = 0, interval: boolean = false): object {
let start: number | null = null; // 记录动画开始的时间戳
function timeElapse(timestamp: number) {
// 定义动画帧回调函数
/*
timestamp参数与 performance.now() 的返回值相同,它表示 requestAnimationFrame() 开始去执行回调函数的时刻
*/
if (!start) {
// 如果还没有开始时间,则以当前时间为开始时间
start = timestamp;
}
const elapsed = timestamp - start;
if (elapsed >= delay) {
try {
fn(); // 执行目标函数
} catch (error) {
console.error('Error executing rafTimeout function:', error);
}
if (interval) {
// 如果需要间隔执行,则重置开始时间并继续安排下一次动画帧
start = timestamp;
raf.id = requestAnimationFrame(timeElapse);
}
} else {
raf.id = requestAnimationFrame(timeElapse);
}
}
interface AnimationFrameID {
id: number
}
// 创建一个对象用于存储动画帧的 ID并初始化动画帧
const raf: AnimationFrameID = {
id: requestAnimationFrame(timeElapse)
};
return raf;
}
/**
* 用于取消 rafTimeout 函数
*
* @param raf - 包含请求动画帧 ID 的对象;该 ID 是由 requestAnimationFrame 返回的
* 该函数旨在取消之前通过 requestAnimationFrame 请求的动画帧
* 如果传入的 raf 对象或其 id 无效,则会打印警告
*/
export function cancelRaf(raf: { id: number }): void {
if (raf && raf.id && typeof raf.id === 'number') {
cancelAnimationFrame(raf.id);
} else {
console.warn('cancelRaf received an invalid id:', raf);
}
}
/**
* 组合式函数
* 使用 ResizeObserver 观察 DOM 元素尺寸变化
*
* 该函数提供了一种方便的方式来观察一个或多个元素的尺寸变化,并在变化时执行指定的回调函数
*
* @param target 要观察的目标,可以是 Ref 对象、Ref 数组、HTMLElement 或 HTMLElement 数组
* @param callback 当元素尺寸变化时调用的回调函数
* @param options ResizeObserver 选项,用于定制观察行为
* @returns 返回一个对象,包含停止和开始观察的方法,使用者可以调用 start 方法开始观察,调用 stop 方法停止观察
*/
export function useResizeObserver(
target: Ref | Ref[] | HTMLElement | HTMLElement[],
callback: ResizeObserverCallback,
options: object = {}
) {
const isSupported = useSupported(() => window && 'ResizeObserver' in window);
let observer: ResizeObserver | undefined;
const stopObservation = ref(false);
const targets = computed(() => {
const targetsValue = toValue(target);
if (targetsValue) {
if (Array.isArray(targetsValue)) {
return targetsValue.map((el: any) => toValue(el)).filter((el: any) => el);
} else {
return [targetsValue];
}
}
return [];
});
// 定义清理函数,用于断开 ResizeObserver 的连接
const cleanup = () => {
if (observer) {
observer.disconnect();
observer = undefined;
}
};
// 初始化 ResizeObserver开始观察目标元素
const observeElements = () => {
if (isSupported.value && targets.value.length && !stopObservation.value) {
observer = new ResizeObserver(callback);
targets.value.forEach((element: HTMLElement) => observer!.observe(element, options));
}
};
// 监听 targets 的变化,当 targets 变化时,重新建立 ResizeObserver 观察
watch(
() => targets.value,
() => {
cleanup();
observeElements();
},
{
immediate: true, // 立即触发回调,以便初始状态也被观察
flush: 'post'
}
);
const stop = () => {
stopObservation.value = true;
cleanup();
};
const start = () => {
stopObservation.value = false;
observeElements();
};
// 在组件卸载前清理 ResizeObserver
onBeforeUnmount(() => cleanup());
return {
stop,
start
};
}
// 辅助函数
export function useSupported(callback: () => unknown) {
const isMounted = useMounted();
return computed(() => {
// to trigger the ref
if (isMounted.value) {
// no-op
}
;
return Boolean(callback());
});
}
export function useMounted() {
const isMounted = ref(false);
// 获取当前组件的实例
const instance = getCurrentInstance();
if (instance) {
onMounted(() => {
isMounted.value = true;
}, instance);
}
return isMounted;
}
/**
* 防抖函数 debounce
*
* 主要用于限制函数调用的频率,当频繁触发某个函数时,实际上只需要在最后一次触发后的一段时间内执行一次即可
* 这对于诸如输入事件处理函数、窗口大小调整事件处理函数等可能会频繁触发的函数非常有用
*
* @param fn 要执行的函数
* @param delay 防抖的时间期限,单位 ms默认为 300ms
* @returns 返回一个新的防抖的函数
*/
export function debounce(fn: (...args: any[]) => any, delay: number = 300): any {
let timer: any = null; // 使用闭包保存定时器的引用
return function (...args: any[]) {
// 返回一个包装函数
if (timer) {
// 如果定时器存在,则清除之前的定时器
clearTimeout(timer);
}
// 设置新的定时器,延迟执行原函数
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
/**
* 组合式函数
* 使用 MutationObserver 观察 DOM 元素的变化
*
* 该函数提供了一个便捷的方式来订阅 DOM 元素的变动,当元素发生指定的变化时,调用提供的回调函数
* 使用者可以指定要观察的一个或多个 DOM 元素,以及观察的选项和回调函数
*
* @param target 要观察的目标,可以是 Ref 对象、Ref 数组、HTMLElement 或 HTMLElement 数组
* @param callback 当观察到变化时调用的回调函数
* @param options MutationObserver 的观察选项,默认为空对象;例如:
* subtree: 是否监听以 target 为根节点的整个子树,包括子树中所有节点的属性
* childList: 是否监听 target 节点中发生的节点的新增与删除
* attributes: 是否观察所有监听的节点属性值的变化
* attributeFilter: 声明哪些属性名会被监听的数组;如果不声明该属性,所有属性的变化都将触发通知
* @returns 返回一个对象,包含停止和开始观察的方法,使用者可以调用 start 方法开始观察,调用 stop 方法停止观察
*/
export function useMutationObserver(
target: Ref | Ref[] | HTMLElement | HTMLElement[],
callback: MutationCallback,
options = {}
) {
const isSupported = useSupported(() => window && 'MutationObserver' in window);
const stopObservation = ref(false);
let observer: MutationObserver | undefined;
const targets = computed(() => {
const targetsValue = toValue(target);
if (targetsValue) {
if (Array.isArray(targetsValue)) {
return targetsValue.map((el: any) => toValue(el)).filter((el: any) => el);
} else {
return [targetsValue];
}
}
return [];
});
// 定义清理函数,用于断开 MutationObserver 的连接
const cleanup = () => {
if (observer) {
observer.disconnect();
observer = undefined;
}
};
// 初始化 MutationObserver开始观察目标元素
const observeElements = () => {
if (isSupported.value && targets.value.length && !stopObservation.value) {
observer = new MutationObserver(callback);
targets.value.forEach((element: HTMLElement) => observer!.observe(element, options));
}
};
// 监听 targets 的变化,当 targets 变化时,重新建立 MutationObserver 观察
watch(
() => targets.value,
() => {
cleanup();
observeElements();
},
{
immediate: true, // 立即触发回调,以便初始状态也被观察
flush: 'post'
}
);
const stop = () => {
stopObservation.value = true;
cleanup();
};
const start = () => {
stopObservation.value = false;
observeElements();
};
// 在组件卸载前清理 MutationObserver
onBeforeUnmount(() => cleanup());
return {
stop,
start
};
}