perf: 更干净整洁的项目配置右侧弹出面板 (#841)

This commit is contained in:
xiaoming 2024-01-01 12:55:39 +08:00 committed by GitHub
parent e7d55ff67e
commit 7acdf03f87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 309 additions and 319 deletions

View File

@ -1,7 +1,5 @@
@import "cropperjs/dist/cropper.css";
@import "tippy.js/dist/tippy.css";
@import "tippy.js/themes/light.css";
@import "tippy.js/animations/perspective.css";
@import "cropperjs/dist/cropper.css";
.re-circled {
.cropper-view-box,

View File

@ -1,10 +1,10 @@
import "./circled.css";
import Cropper from "cropperjs";
import { useTippy } from "vue-tippy";
import { ElUpload } from "element-plus";
import type { CSSProperties } from "vue";
import { useResizeObserver } from "@vueuse/core";
import { longpress } from "@/directives/longpress";
import { useTippy, directive as tippy } from "vue-tippy";
import { delay, debounce, isArray, downloadByBase64 } from "@pureadmin/utils";
import {
ref,
@ -233,7 +233,6 @@ export default defineComponent({
const menuContent = defineComponent({
directives: {
tippy,
longpress
},
setup() {

View File

@ -33,7 +33,7 @@ export function useRenderIcon(icon: any, attrs?: iconType): Component {
});
} else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg
return icon;
return h(icon, { ...attrs });
} else if (typeof icon === "object") {
return defineComponent({
name: "OfflineIcon",

View File

@ -13,7 +13,8 @@ export interface iconType {
align?: string;
onLoad?: Function;
includes?: Function;
// all icon
// svg 需要什么SVG属性自行添加
fill?: string;
// all icon
style?: object;
}

View File

@ -6,7 +6,6 @@
color: rgba(0, 0, 0, 0.65);
background-color: rgb(0 0 0 / 4%);
border-radius: 2px;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.pure-segmented-group {
@ -43,7 +42,7 @@
text-align: center;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.pure-segmented-item > div {

View File

@ -121,7 +121,11 @@ export default defineComponent({
class="pure-segmented-item-icon"
style={{ marginRight: option.label ? "6px" : 0 }}
>
{h(useRenderIcon(option.icon))}
{h(
useRenderIcon(option.icon, {
...option?.iconAttrs
})
)}
</span>
) : null}
{option.label ? (

View File

@ -1,4 +1,5 @@
import type { VNode, Component } from "vue";
import type { iconType } from "@/components/ReIcon/src/types.ts";
export interface OptionsType {
/** 文字 */
@ -8,6 +9,8 @@ export interface OptionsType {
* @see {@link https://yiming_chang.gitee.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
*/
icon?: string | Component;
/** 图标属性、样式配置 */
iconAttrs?: iconType;
/** 值 */
value?: string | number;
/** 是否禁用 */

View File

@ -2,6 +2,7 @@
import { emitter } from "@/utils/mitt";
import { onClickOutside } from "@vueuse/core";
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import Close from "@iconify-icons/ep/close";
const target = ref(null);
@ -9,7 +10,6 @@ const show = ref<Boolean>(false);
const iconClass = computed(() => {
return [
"mr-[20px]",
"outline-none",
"width-[20px]",
"height-[20px]",
@ -22,6 +22,8 @@ const iconClass = computed(() => {
];
});
const { onReset } = useDataThemeChange();
onClickOutside(target, (event: any) => {
if (event.clientX > target.value.offsetLeft) return;
show.value = false;
@ -43,23 +45,47 @@ onBeforeUnmount(() => {
<div :class="{ show: show }" class="right-panel-container">
<div class="right-panel-background" />
<div ref="target" class="right-panel bg-bg_color">
<div class="right-panel-items">
<div class="project-configuration">
<h4 class="dark:text-white">项目配置</h4>
<span title="关闭配置" :class="iconClass">
<IconifyIconOffline
class="dark:text-white"
width="20px"
height="20px"
:icon="Close"
@click="show = !show"
/>
</span>
</div>
<div
class="border-b-[1px] border-solid border-[#dcdfe6] dark:border-[#303030]"
/>
<div
class="project-configuration border-b-[1px] border-solid border-[var(--pure-border-color)]"
>
<h4 class="dark:text-white">项目配置</h4>
<span
v-tippy="{
content: '关闭配置',
placement: 'bottom-start',
zIndex: 41000
}"
:class="iconClass"
>
<IconifyIconOffline
class="dark:text-white"
width="20px"
height="20px"
:icon="Close"
@click="show = !show"
/>
</span>
</div>
<el-scrollbar>
<slot />
</el-scrollbar>
<div
class="flex justify-end p-3 border-t-[1px] border-solid border-[var(--pure-border-color)]"
>
<el-button
v-tippy="{
content: '清空缓存并返回登录页',
placement: 'left-start',
zIndex: 41000
}"
type="danger"
text
bg
@click="onReset"
>
清空缓存
</el-button>
</div>
</div>
</div>
@ -74,6 +100,10 @@ onBeforeUnmount(() => {
</style>
<style lang="scss" scoped>
:deep(.el-scrollbar) {
height: calc(100vh - 110px);
}
.right-panel-background {
position: fixed;
top: 0;
@ -90,7 +120,7 @@ onBeforeUnmount(() => {
right: 0;
z-index: 40000;
width: 100%;
max-width: 315px;
max-width: 300px;
height: 100vh;
box-shadow: 0 0 15px 0 rgb(0 0 0 / 5%);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
@ -112,47 +142,10 @@ onBeforeUnmount(() => {
}
}
.handle-button {
position: absolute;
top: 45%;
left: -48px;
z-index: 0;
width: 48px;
height: 48px;
font-size: 24px;
line-height: 48px;
color: #fff;
text-align: center;
pointer-events: auto;
cursor: pointer;
background: rgb(24 144 255);
border-radius: 6px 0 0 6px !important;
i {
font-size: 24px;
line-height: 48px;
}
}
.right-panel-items {
height: calc(100vh - 60px);
margin-top: 60px;
overflow-y: auto;
}
.project-configuration {
position: fixed;
top: 15px;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 30px;
margin-left: 10px;
}
:deep(.el-divider--horizontal) {
width: 90%;
margin: 20px auto 0;
padding: 14px 20px;
}
</style>

View File

@ -8,28 +8,22 @@ import {
nextTick,
onBeforeMount
} from "vue";
import { getConfig } from "@/config";
import { useRouter } from "vue-router";
import panel from "../panel/index.vue";
import { emitter } from "@/utils/mitt";
import { resetRouter } from "@/router";
import { removeToken } from "@/utils/auth";
import { routerArrays } from "@/layout/types";
import { useNav } from "@/layout/hooks/useNav";
import { useAppStoreHook } from "@/store/modules/app";
import { useDark, debounce, useGlobal } from "@pureadmin/utils";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { useDark, debounce, useGlobal, storageLocal } from "@pureadmin/utils";
import Check from "@iconify-icons/ep/check";
import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component";
import Check from "@iconify-icons/ep/check";
import Logout from "@iconify-icons/ri/logout-circle-r-line";
const router = useRouter();
const { device } = useNav();
const { isDark } = useDark();
const { device, tooltipEffect } = useNav();
const { $storage } = useGlobal<GlobalPropertiesApi>();
const mixRef = ref();
@ -40,8 +34,8 @@ const {
dataTheme,
layoutTheme,
themeColors,
toggleClass,
dataThemeChange,
setEpThemeColor,
setLayoutThemeColor
} = useDataThemeChange();
@ -89,13 +83,6 @@ function storageConfigureChange<T>(key: string, val: T): void {
$storage.configure = storageConfigure;
}
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
const targetEl = target || document.body;
let { className } = targetEl;
className = className.replace(clsName, "").trim();
targetEl.className = flag ? `${className} ${clsName} ` : className;
}
/** 灰色模式设置 */
const greyChange = (value): void => {
toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
@ -132,24 +119,11 @@ const multiTagsCacheChange = () => {
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
};
/** 清空缓存并返回登录页 */
function onReset() {
removeToken();
storageLocal().clear();
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
useAppStoreHook().setLayout(Layout);
setEpThemeColor(EpThemeColor);
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
toggleClass(Grey, "html-grey", document.querySelector("html"));
toggleClass(Weak, "html-weakness", document.querySelector("html"));
router.push("/login");
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter();
}
function onChange(label) {
storageConfigureChange("showModel", label);
emitter.emit("tagViewsShowModel", label);
function onChange({ option }) {
const { value } = option;
markValue.value = value;
storageConfigureChange("showModel", value);
emitter.emit("tagViewsShowModel", value);
}
/** 侧边栏Logo */
@ -185,6 +159,32 @@ const getThemeColor = computed(() => {
};
});
const themeOptions = computed<Array<OptionsType>>(() => {
return [
{
label: "亮色",
icon: dayIcon,
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
},
{
label: "暗色",
icon: darkIcon,
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
}
];
});
const markOptions: Array<OptionsType> = [
{
label: "灵动",
value: "smart"
},
{
label: "卡片",
value: "card"
}
];
/** 设置导航模式 */
function setLayoutModel(layout: string) {
layoutTheme.value.layout = layout;
@ -234,185 +234,153 @@ onBeforeMount(() => {
<template>
<panel>
<el-divider>主题</el-divider>
<el-switch
v-model="dataTheme"
inline-prompt
class="pure-datatheme"
:active-icon="dayIcon"
:inactive-icon="darkIcon"
@change="dataThemeChange"
/>
<div class="p-6">
<p class="mb-3 font-medium text-sm dark:text-white">整体风格</p>
<Segmented
:modelValue="dataTheme ? 1 : 0"
:options="themeOptions"
@change="
{
dataTheme = !dataTheme;
dataThemeChange();
}
"
/>
<el-divider>导航栏模式</el-divider>
<ul class="pure-theme">
<el-tooltip
:effect="tooltipEffect"
class="item"
content="左侧模式"
placement="bottom"
popper-class="pure-tooltip"
>
<p class="mt-5 mb-3 font-medium text-sm dark:text-white">主题色</p>
<ul class="theme-color">
<li
v-for="(item, index) in themeColors"
v-show="showThemeColors(item.themeColor)"
:key="index"
:style="getThemeColorStyle(item.color)"
@click="setLayoutThemeColor(item.themeColor)"
>
<el-icon
style="margin: 0.1em 0.1em 0 0"
:size="17"
:color="getThemeColor(item.themeColor)"
>
<IconifyIconOffline :icon="Check" />
</el-icon>
</li>
</ul>
<p class="mt-5 mb-3 font-medium text-sm dark:text-white">导航模式</p>
<ul class="pure-theme">
<li
ref="verticalRef"
v-tippy="{
content: '左侧菜单',
zIndex: 41000
}"
:class="layoutTheme.layout === 'vertical' ? 'is-select' : ''"
@click="setLayoutModel('vertical')"
>
<div />
<div />
</li>
</el-tooltip>
<el-tooltip
v-if="device !== 'mobile'"
:effect="tooltipEffect"
class="item"
content="顶部模式"
placement="bottom"
popper-class="pure-tooltip"
>
<li
v-if="device !== 'mobile'"
ref="horizontalRef"
v-tippy="{
content: '顶部菜单',
zIndex: 41000
}"
:class="layoutTheme.layout === 'horizontal' ? 'is-select' : ''"
@click="setLayoutModel('horizontal')"
>
<div />
<div />
</li>
</el-tooltip>
<el-tooltip
v-if="device !== 'mobile'"
:effect="tooltipEffect"
class="item"
content="混合模式"
placement="bottom"
popper-class="pure-tooltip"
>
<li
v-if="device !== 'mobile'"
ref="mixRef"
v-tippy="{
content: '混合菜单',
zIndex: 41000
}"
:class="layoutTheme.layout === 'mix' ? 'is-select' : ''"
@click="setLayoutModel('mix')"
>
<div />
<div />
</li>
</el-tooltip>
</ul>
</ul>
<el-divider>主题色</el-divider>
<ul class="theme-color">
<li
v-for="(item, index) in themeColors"
v-show="showThemeColors(item.themeColor)"
:key="index"
:style="getThemeColorStyle(item.color)"
@click="setLayoutThemeColor(item.themeColor)"
>
<el-icon
style="margin: 0.1em 0.1em 0 0"
:size="17"
:color="getThemeColor(item.themeColor)"
>
<IconifyIconOffline :icon="Check" />
</el-icon>
</li>
</ul>
<el-divider>界面显示</el-divider>
<ul class="setting">
<li>
<span class="dark:text-white">灰色模式</span>
<el-switch
v-model="settings.greyVal"
inline-prompt
inactive-color="#a6a6a6"
active-text="开"
inactive-text="关"
@change="greyChange"
/>
</li>
<li>
<span class="dark:text-white">色弱模式</span>
<el-switch
v-model="settings.weakVal"
inline-prompt
inactive-color="#a6a6a6"
active-text="开"
inactive-text="关"
@change="weekChange"
/>
</li>
<li>
<span class="dark:text-white">隐藏标签页</span>
<el-switch
v-model="settings.tabsVal"
inline-prompt
inactive-color="#a6a6a6"
active-text="开"
inactive-text="关"
@change="tagsChange"
/>
</li>
<li>
<span class="dark:text-white">隐藏页脚</span>
<el-switch
v-model="settings.hideFooter"
inline-prompt
inactive-color="#a6a6a6"
active-text="开"
inactive-text="关"
@change="hideFooterChange"
/>
</li>
<li>
<span class="dark:text-white">侧边栏Logo</span>
<el-switch
v-model="logoVal"
inline-prompt
:active-value="true"
:inactive-value="false"
inactive-color="#a6a6a6"
active-text="开"
inactive-text="关"
@change="logoChange"
/>
</li>
<li>
<span class="dark:text-white">标签页持久化</span>
<el-switch
v-model="settings.multiTagsCache"
inline-prompt
inactive-color="#a6a6a6"
active-text="开"
inactive-text="关"
@change="multiTagsCacheChange"
/>
</li>
<li>
<span class="dark:text-white">标签风格</span>
<el-radio-group v-model="markValue" size="small" @change="onChange">
<el-radio label="card">卡片</el-radio>
<el-radio label="smart">灵动</el-radio>
</el-radio-group>
</li>
</ul>
<el-divider />
<el-button
type="danger"
style="width: 90%; margin: 24px 15px"
@click="onReset"
>
<IconifyIconOffline
:icon="Logout"
width="15"
height="15"
style="margin-right: 4px"
<p class="mt-5 mb-3 font-medium text-base dark:text-white">页签风格</p>
<Segmented
:modelValue="markValue === 'smart' ? 0 : 1"
:options="markOptions"
@change="onChange"
/>
清空缓存并返回登录页
</el-button>
<p class="mt-5 mb-1 font-medium text-sm dark:text-white">界面显示</p>
<ul class="setting">
<li>
<span class="dark:text-white">灰色模式</span>
<el-switch
v-model="settings.greyVal"
inline-prompt
active-text="开"
inactive-text="关"
@change="greyChange"
/>
</li>
<li>
<span class="dark:text-white">色弱模式</span>
<el-switch
v-model="settings.weakVal"
inline-prompt
active-text="开"
inactive-text="关"
@change="weekChange"
/>
</li>
<li>
<span class="dark:text-white">隐藏标签页</span>
<el-switch
v-model="settings.tabsVal"
inline-prompt
active-text="开"
inactive-text="关"
@change="tagsChange"
/>
</li>
<li>
<span class="dark:text-white">隐藏页脚</span>
<el-switch
v-model="settings.hideFooter"
inline-prompt
active-text="开"
inactive-text="关"
@change="hideFooterChange"
/>
</li>
<li>
<span class="dark:text-white">Logo</span>
<el-switch
v-model="logoVal"
inline-prompt
:active-value="true"
:inactive-value="false"
active-text="开"
inactive-text="关"
@change="logoChange"
/>
</li>
<li>
<span class="dark:text-white">页签持久化</span>
<el-switch
v-model="settings.multiTagsCache"
inline-prompt
active-text="开"
inactive-text="关"
@change="multiTagsCacheChange"
/>
</li>
</ul>
</div>
</panel>
</template>
@ -422,41 +390,41 @@ onBeforeMount(() => {
font-weight: 700;
}
.is-select {
border: 2px solid var(--el-color-primary);
:deep(.el-switch__core) {
--el-switch-off-color: var(--pure-switch-off-color);
min-width: 36px;
height: 18px;
}
.setting {
width: 100%;
:deep(.el-switch__core .el-switch__action) {
height: 14px;
}
.theme-color {
height: 20px;
li {
display: flex;
align-items: center;
justify-content: space-between;
margin: 25px;
}
}
float: left;
height: 20px;
margin-right: 8px;
cursor: pointer;
border-radius: 4px;
.pure-datatheme {
display: block;
width: 100%;
height: 50px;
padding-top: 25px;
text-align: center;
&:nth-child(2) {
border: 1px solid #ddd;
}
}
}
.pure-theme {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
width: 100%;
height: 50px;
margin-top: 25px;
gap: 12px;
li {
position: relative;
width: 18%;
height: 45px;
width: 46px;
height: 36px;
overflow: hidden;
cursor: pointer;
background: #f0f2f5;
@ -517,27 +485,17 @@ onBeforeMount(() => {
}
}
.theme-color {
display: flex;
justify-content: center;
width: 100%;
height: 40px;
margin-top: 20px;
.is-select {
border: 2px solid var(--el-color-primary);
}
.setting {
li {
float: left;
width: 20px;
height: 20px;
margin-top: 8px;
margin-right: 8px;
font-weight: 700;
text-align: center;
cursor: pointer;
border-radius: 2px;
&:nth-child(2) {
border: 1px solid #ddd;
}
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
font-size: 14px;
}
}
</style>

View File

@ -1,9 +1,14 @@
import { ref } from "vue";
import { getConfig } from "@/config";
import { useLayout } from "./useLayout";
import { useGlobal } from "@pureadmin/utils";
import { removeToken } from "@/utils/auth";
import { routerArrays } from "@/layout/types";
import { router, resetRouter } from "@/router";
import type { themeColorsType } from "../types";
import { useAppStoreHook } from "@/store/modules/app";
import { useGlobal, storageLocal } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import {
darken,
lighten,
@ -37,6 +42,13 @@ export function useDataThemeChange() {
const dataTheme = ref<boolean>($storage?.layout?.darkMode);
const body = document.documentElement as HTMLElement;
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
const targetEl = target || document.body;
let { className } = targetEl;
className = className.replace(clsName, "").trim();
targetEl.className = flag ? `${className} ${clsName} ` : className;
}
/** 设置导航主题色 */
function setLayoutThemeColor(theme = getConfig().Theme ?? "default") {
layoutTheme.value.theme = theme;
@ -78,9 +90,8 @@ export function useDataThemeChange() {
}
};
/** 日间、夜间主题切换 */
/** 亮色、暗色整体风格切换 */
function dataThemeChange() {
/* 如果当前是light夜间主题默认切换到default主题 */
if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
setLayoutThemeColor("default");
} else {
@ -94,11 +105,28 @@ export function useDataThemeChange() {
}
}
/** 清空缓存并返回登录页 */
function onReset() {
removeToken();
storageLocal().clear();
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
useAppStoreHook().setLayout(Layout);
setEpThemeColor(EpThemeColor);
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
toggleClass(Grey, "html-grey", document.querySelector("html"));
toggleClass(Weak, "html-weakness", document.querySelector("html"));
router.push("/login");
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter();
}
return {
body,
dataTheme,
layoutTheme,
themeColors,
onReset,
toggleClass,
dataThemeChange,
setEpThemeColor,
setLayoutThemeColor

View File

@ -45,6 +45,14 @@ app.component("FontIcon", FontIcon);
import { Auth } from "@/components/ReAuth";
app.component("Auth", Auth);
// 全局注册`vue-tippy`
import "tippy.js/dist/tippy.css";
import "tippy.js/animations/perspective.css";
import VueTippy from "vue-tippy";
app.use(VueTippy, {
defaultProps: { animation: "perspective" }
});
getPlatformConfig(app).then(async config => {
setupStore(app);
app.use(router);

View File

@ -2,11 +2,18 @@
/* 暗黑模式适配 */
html.dark {
/* 自定义深色背景颜色 */
// --el-bg-color: #020409;
$border-style: #303030;
$color-white: #fff;
/* 自定义深色背景颜色 */
// --el-bg-color: #020409;
/* 常用border-color 需要时可取用 */
--pure-border-color: rgb(253 253 253 / 12%);
/* switch关闭状态下的color 需要时可取用 */
--pure-switch-off-color: #ffffff3f;
.navbar,
.tags-view,
.contextmenu,

View File

@ -50,12 +50,6 @@
padding: 0 !important;
}
/* 自定义 tooltip 的类名 */
.pure-tooltip {
// 右侧操作面板right-panel类名的z-index为40000tooltip需要大于它才能显示
z-index: 41000 !important;
}
/* nprogress 适配 element-plus 的主题色 */
#nprogress {
& .bar {

View File

@ -7,6 +7,12 @@
:root {
/* 左侧菜单展开、收起动画时长 */
--pure-transition-duration: 0.3s;
/* 常用border-color 需要时可取用 */
--pure-border-color: rgb(5 5 5 / 6%);
/* switch关闭状态下的color 需要时可取用 */
--pure-switch-off-color: #a6a6a6;
}
/* 灰色模式 */

View File

@ -4,15 +4,10 @@ import { getTime } from "@pureadmin/utils";
import { Play, Pause, Forward, Rewind } from "./svg";
import { ref, onMounted, onBeforeUnmount } from "vue";
import "tippy.js/dist/tippy.css";
import "tippy.js/animations/scale.css";
import { directive as tippy } from "vue-tippy";
defineOptions({
name: "Wavesurfer"
});
const vTippy = tippy;
const loading = ref(true);
const wavesurfer = ref(null);
const wavesurferRef = ref();
@ -114,8 +109,7 @@ onBeforeUnmount(() => {
<Rewind
v-tippy="{
content: '快退(可长按)',
placement: 'bottom',
animation: 'scale'
placement: 'bottom'
}"
v-longpress:0:100="() => wavesurfer?.skip(-1)"
class="cursor-pointer"
@ -123,8 +117,7 @@ onBeforeUnmount(() => {
<div
v-tippy="{
content: isPlay ? '暂停' : '播放',
placement: 'bottom',
animation: 'scale'
placement: 'bottom'
}"
class="cursor-pointer"
@click="wavesurfer?.playPause()"
@ -135,8 +128,7 @@ onBeforeUnmount(() => {
<Forward
v-tippy="{
content: '快进(可长按)',
placement: 'bottom',
animation: 'scale'
placement: 'bottom'
}"
v-longpress:0:100="() => wavesurfer?.skip(1)"
class="cursor-pointer"