mirror of
https://github.com/MCSManager/MCSManager.git
synced 2025-02-05 15:29:35 +08:00
Feat: music player card
This commit is contained in:
parent
f1a6051bd8
commit
4b06cd42cc
@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useLayoutContainerStore } from "@/stores/useLayoutContainerStore";
|
||||
|
||||
const props = defineProps({
|
||||
fullHeight: {
|
||||
type: Boolean,
|
||||
@ -9,6 +11,8 @@ const props = defineProps({
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const { containerState } = useLayoutContainerStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -29,11 +33,13 @@ const props = defineProps({
|
||||
<div>
|
||||
<a-typography-text>
|
||||
<slot name="operator"></slot>
|
||||
<slot v-if="containerState.isDesignMode" name="operator-design"></slot>
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-panel-content">
|
||||
<slot name="body"></slot>
|
||||
<slot v-if="containerState.isDesignMode" name="body-design"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
0
frontend/src/hooks/widgets/useMusicCard.ts
Normal file
0
frontend/src/hooks/widgets/useMusicCard.ts
Normal file
@ -4,7 +4,6 @@ import { $t as t } from "@/lang/i18n";
|
||||
import type { LayoutCard } from "@/types";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { UndoOutlined } from "@ant-design/icons-vue";
|
||||
import { useLayoutContainerStore } from "@/stores/useLayoutContainerStore";
|
||||
import dayjs from "dayjs";
|
||||
import { useLayoutCardTools } from "@/hooks/useCardTools";
|
||||
import Style1 from "@/components/time/Style1.vue";
|
||||
@ -25,8 +24,6 @@ const changeStyle = () => {
|
||||
setMetaValue("style", (showStyle.value = showStyle.value === maxStyle ? 1 : showStyle.value + 1));
|
||||
};
|
||||
|
||||
const { containerState } = useLayoutContainerStore();
|
||||
|
||||
const isSpinning = ref(false);
|
||||
|
||||
let timekeeping: NodeJS.Timeout | null = null;
|
||||
@ -63,19 +60,17 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div class="h-100">
|
||||
<card-panel>
|
||||
<template #operator>
|
||||
<div v-if="containerState.isDesignMode" class="ml-10">
|
||||
<a-button type="link" size="small" @click="changeStyle()">
|
||||
<template #icon>
|
||||
<a-tooltip placement="top">
|
||||
<template #title>
|
||||
<span>{{ t("更换样式") }}</span>
|
||||
</template>
|
||||
<undo-outlined :rotate="45" :spin="isSpinning" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<template #operator-design>
|
||||
<a-button type="link" size="small" class="ml-10" @click="changeStyle()">
|
||||
<template #icon>
|
||||
<a-tooltip placement="top">
|
||||
<template #title>
|
||||
<span>{{ t("更换样式") }}</span>
|
||||
</template>
|
||||
<undo-outlined :rotate="45" :spin="isSpinning" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #title>{{ card.title }}</template>
|
||||
<template #body>
|
||||
|
@ -1,77 +1,203 @@
|
||||
<!-- eslint-disable no-unused-vars -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { $t as t } from "@/lang/i18n";
|
||||
import CardPanel from "@/components/CardPanel.vue";
|
||||
import { useLayoutContainerStore } from "@/stores/useLayoutContainerStore";
|
||||
import { ref, h } from "vue";
|
||||
import { Empty, message } from "ant-design-vue";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import { PlayCircleOutlined, PauseCircleOutlined } from "@ant-design/icons-vue";
|
||||
import { t } from "@/lang/i18n";
|
||||
import { useUploadFileDialog } from "@/components/fc";
|
||||
import { useAppToolsStore } from "@/stores/useAppToolsStore";
|
||||
import { useLayoutCardTools } from "@/hooks/useCardTools";
|
||||
import type { LayoutCard } from "@/types/index";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
import type { LayoutCard } from "@/types";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
enum EDIT_MODE {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
PREVIEW = "PREVIEW",
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
EDIT = "EDIT"
|
||||
}
|
||||
dayjs.extend(duration);
|
||||
|
||||
const props = defineProps<{
|
||||
const prop = defineProps<{
|
||||
card: LayoutCard;
|
||||
}>();
|
||||
|
||||
const { getMetaValue, setMetaValue } = useLayoutCardTools(props.card);
|
||||
const { containerState } = useLayoutContainerStore();
|
||||
const { openInputDialog } = useAppToolsStore();
|
||||
|
||||
const textContent = ref<string>(getMetaValue("textContent", ""));
|
||||
const status = ref(EDIT_MODE.PREVIEW);
|
||||
const { getMetaValue, setMetaValue } = useLayoutCardTools(prop.card);
|
||||
|
||||
// function
|
||||
const previewsTextContent = () => {
|
||||
setMetaValue("textContent", textContent.value);
|
||||
status.value = EDIT_MODE.PREVIEW;
|
||||
const musicUrl = ref(getMetaValue<string>("musicUrl", ""));
|
||||
|
||||
enum UploadType {
|
||||
File = "FILE",
|
||||
Url = "URL"
|
||||
}
|
||||
|
||||
const uploadMusic = async (type: UploadType) => {
|
||||
try {
|
||||
if (type === UploadType.File) {
|
||||
musicUrl.value = (await useUploadFileDialog()) || musicUrl.value;
|
||||
}
|
||||
if (type === UploadType.Url) {
|
||||
musicUrl.value =
|
||||
((await openInputDialog(t("请输入文件 URL 地址"))) as string) || musicUrl.value;
|
||||
}
|
||||
} catch (error) {}
|
||||
setMetaValue("musicUrl", musicUrl.value);
|
||||
message.success(t("设置成功,保存以应用更改"));
|
||||
};
|
||||
|
||||
const editTextContent = () => {
|
||||
status.value = EDIT_MODE.EDIT;
|
||||
let time: null | HTMLAreaElement = null;
|
||||
|
||||
let wavesurfer: WaveSurfer | null = null;
|
||||
|
||||
enum PlayerStatus {
|
||||
Play = "PLAY",
|
||||
Pause = "PAUSE"
|
||||
}
|
||||
|
||||
const playerStatus = ref(PlayerStatus.Pause);
|
||||
const playerButtonIcon = ref(h(PlayCircleOutlined));
|
||||
const changePlayerStatis = (setStatus?: PlayerStatus) => {
|
||||
if (setStatus) {
|
||||
playerStatus.value = setStatus;
|
||||
} else {
|
||||
playerStatus.value =
|
||||
playerStatus.value === PlayerStatus.Play ? PlayerStatus.Pause : PlayerStatus.Play;
|
||||
}
|
||||
|
||||
if (playerStatus.value === PlayerStatus.Play) {
|
||||
wavesurfer?.play();
|
||||
playerButtonIcon.value = h(PauseCircleOutlined);
|
||||
} else {
|
||||
wavesurfer?.pause();
|
||||
playerButtonIcon.value = h(PlayCircleOutlined);
|
||||
}
|
||||
};
|
||||
const playTime = ref("0");
|
||||
const maxTime = ref("0");
|
||||
|
||||
const s2m = (s: number = 0) => {
|
||||
const duration = dayjs.duration(s, "seconds");
|
||||
return duration.asHours() >= 1 ? duration.format("H:mm:ss") : duration.format("m:ss");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (time) {
|
||||
wavesurfer = WaveSurfer.create({
|
||||
container: time || "",
|
||||
waveColor: "#7a7a7a",
|
||||
progressColor: "#000",
|
||||
url: musicUrl.value,
|
||||
height: 50,
|
||||
barWidth: 2,
|
||||
barGap: 1,
|
||||
barRadius: 2
|
||||
});
|
||||
|
||||
wavesurfer.on("ready", function () {
|
||||
maxTime.value = s2m(wavesurfer?.getDuration());
|
||||
});
|
||||
wavesurfer.on("click", () => {
|
||||
changePlayerStatis(PlayerStatus.Play);
|
||||
});
|
||||
wavesurfer.on("pause", () => {
|
||||
changePlayerStatis(PlayerStatus.Pause);
|
||||
});
|
||||
wavesurfer.on("finish", () => {
|
||||
changePlayerStatis(PlayerStatus.Play);
|
||||
});
|
||||
wavesurfer.on("timeupdate", (currentTime) => {
|
||||
playTime.value = s2m(currentTime);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardPanel>
|
||||
<template #title>
|
||||
<div class="flex">{{ card.title }}</div>
|
||||
</template>
|
||||
<template #operator>
|
||||
<div v-if="containerState.isDesignMode" class="ml-10">
|
||||
<a-button
|
||||
v-if="status !== EDIT_MODE.PREVIEW"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="previewsTextContent()"
|
||||
>
|
||||
{{ t("TXT_CODE_4d81a657") }}
|
||||
</a-button>
|
||||
<a-button v-else type="primary" size="small" @click="editTextContent()">
|
||||
{{ t("TXT_CODE_ad207008") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-100 position-relative">
|
||||
<card-panel>
|
||||
<template #title>
|
||||
{{ card.title }}
|
||||
</template>
|
||||
|
||||
<template v-if="containerState.isDesignMode && status == EDIT_MODE.EDIT" #body>
|
||||
<div class="edit h-100">
|
||||
<a-textarea
|
||||
v-model:value="textContent"
|
||||
class="h-100"
|
||||
style="resize: none"
|
||||
:placeholder="t('TXT_CODE_7ceebc05')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else #body>
|
||||
<div class="full-card-body-container">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="previews global-markdown-html h-100" v-html="markdownToHTML(textContent)"></div>
|
||||
</div>
|
||||
</template>
|
||||
</CardPanel>
|
||||
<template #body>
|
||||
<div v-if="musicUrl" class="h-100 flex-center">
|
||||
<div class="player">
|
||||
<div class="button">
|
||||
<a-button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
:icon="playerButtonIcon"
|
||||
@click="changePlayerStatis()"
|
||||
/>
|
||||
</div>
|
||||
<div class="time-line">
|
||||
<div ref="time" class="time"></div>
|
||||
{{ playTime }} / {{ maxTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-empty class="h-100" :image="Empty.PRESENTED_IMAGE_SIMPLE">
|
||||
<template #description>
|
||||
<span>{{ t("暂无音乐") }}</span>
|
||||
<br />
|
||||
<span>{{ t("使用设计模式将鼠标移动到此处以进行编辑") }}</span>
|
||||
</template>
|
||||
</a-empty>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-design>
|
||||
<a-space align="center" direction="vertical" class="w-100 h-100 edit">
|
||||
<h1>修改曲目</h1>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="uploadMusic(UploadType.File)">
|
||||
{{ t("上传音乐文件") }}
|
||||
</a-button>
|
||||
<a-button type="primary" @click="uploadMusic(UploadType.Url)">
|
||||
{{ t("填写音乐 URL") }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</template>
|
||||
</card-panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.edit {
|
||||
z-index: 5;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px #000;
|
||||
transition: all 0.1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.player {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.time-line {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
8
package-lock.json
generated
8
package-lock.json
generated
@ -9,7 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"crc": "^4.3.2",
|
||||
"i18next-scanner": "^4.4.0"
|
||||
"i18next-scanner": "^4.4.0",
|
||||
"wavesurfer.js": "^7.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
@ -902,6 +903,11 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wavesurfer.js": {
|
||||
"version": "7.6.4",
|
||||
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.6.4.tgz",
|
||||
"integrity": "sha512-ZpGOHzFeShTD02OoXNSoo9hfHM7awPckNjlRuCbLb9eKcHTJB8tEE+REkNOwJKJ46uo0cT7VeRbMlVvKgzUV/w=="
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"crc": "^4.3.2",
|
||||
"i18next-scanner": "^4.4.0"
|
||||
"i18next-scanner": "^4.4.0",
|
||||
"wavesurfer.js": "^7.6.4"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user