自封装的组件
大约 4 分钟
录音组件
查看代码
useVModel
import { computed } from "vue";
export const useVModel = (props: AnyObject, propName: string, emit: (...args: any) => void) => {
return computed({
get() {
return new Proxy(props, {
set(obj, name, val) {
emit("update:" + propName, {
...obj,
[name]: val,
});
return true;
},
});
},
set(val) {
emit("update:" + propName, val);
},
});
};
使用示例
<template>
<Record v-model="showRecord" @complete="recordComplete" />
</template>
<script setup lang="ts">
//是否显示录音组件
const showRecord = ref<boolean>(false);
// 录制完成
const recordComplete = (path: string) => {
};
</script>
组件源码
<template>
<tn-popup v-model="popupParam.modelValue" :overlay-closeable="false" @open="openRecordComp">
<view class="tn-p-lg record-panel" @touchmove.prevent="() => {}">
<view class="tn-flex-start-between">
<text class="tn-text-2xl tn-mb">录制音频</text>
<tn-icon name="close-circle" type="info" @click="closeRecord" :size="useVmin(50)" />
</view>
<view class="record-info">
<text>
录制状态:
<text :style="{ color: recordingStatus.color }">{{ recordingStatus.text }}</text>
</text>
<text>
最长可录制时长:
<text>{{ recordingStatus.totalSeconds }}s</text>
</text>
<text>录制时长:{{ recordingStatus.recordSeconds === 0 ? "-" : recordingStatus.recordSeconds + "s" }}</text>
</view>
<view class="tn-flex-center-between record-controller">
<tn-button type="primary" @tap="startRecord" :disabled="recordingStatus.status !== 0">
<view class="tn-flex-center">
<tn-icon name="play-fill" />
<text>开始录音</text>
</view>
</tn-button>
<tn-button type="success" @tap="recordComplate" :disabled="recordingStatus.status === 0">
<view class="tn-flex-center">
<tn-icon name="check" />
<text>录音完成</text>
</view>
</tn-button>
</view>
</view>
</tn-popup>
</template>
<script setup lang="ts">
import { reactive, inject, type Ref, nextTick } from "vue";
import type { TnModalInstance } from "@tuniao/tnui-vue3-uniapp";
import { useVmin, useVModel } from "@/utils/hooks";
import { useUserInfoStore } from "@/store";
const userInfoStore = useUserInfoStore();
const modalRef = inject<Ref<TnModalInstance>>("modalRef") as Ref<TnModalInstance>;
const props = withDefaults(
defineProps<{
modelValue: boolean;
}>(),
{
modelValue: false,
},
);
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
(e: "complete", path: string): void;
}>();
const popupParam = useVModel(props, "modelValue", emit);
enum RecordStatus {
"录制取消" = -1,
"未开始",
"录制中",
}
interface RecordingParams {
/**
* 录制状态
*/
text: string;
/**
* 录制状态文本颜色
*/
color: "var(--tn-color-primary)" | "var(--tn-color-success)";
/**
* 录制状态标识
*/
status: -1 | 0 | 1;
/**
* 总共允许录制的时长
*/
totalSeconds: number;
/**
* 已录制的时长
*/
recordSeconds: number;
}
const options: RecordingParams = {
text: "未开始",
color: "var(--tn-color-primary)",
status: 0,
totalSeconds: userInfoStore.userConf.tapeDuration,
recordSeconds: 0,
};
const recordingStatus = reactive<RecordingParams>(JSON.parse(JSON.stringify(options)));
let timer: NodeJS.Timeout;
let recorderManager: UniApp.RecorderManager | null;
/**
* 开始录制
*/
const startRecord = () => {
if (recordingStatus.totalSeconds === 0) {
uni.showToast({
icon: "none",
title: "录音最长时间获取为0,不可进行录音!",
});
return;
}
// 录音实例只能全局创建一次,不能多次创建,会影响onStop函数里面的监听,尤其读取外面声明的变量
recorderManager = uni.getRecorderManager?.();
// 监听录音结束
recorderManager?.onStop(async (file: { tempFilePath: string }) => {
await nextTick();
emit("update:modelValue", false);
recordingStatus.status !== -1 && emit("complete", file.tempFilePath);
recorderManager = null;
});
recorderManager?.start({
format: "mp3",
sampleRate: 8000, //采样率
duration: recordingStatus.totalSeconds * 1000, //时长
});
countDown();
recordingStatus.status = 1;
recordingStatus.text = RecordStatus[recordingStatus.status];
recordingStatus.color = "var(--tn-color-success)";
};
/**
* 录制完成
* @param isOvertime 是否超时
*/
const recordComplate = (isOvertime?: boolean) => {
timer && clearInterval(timer);
// 录制超时不用手动调用stop,超时会自动走recorderManager?.onStop
!isOvertime && recorderManager?.stop();
};
/**
* 关闭组件
*/
const closeRecord = () => {
if (recordingStatus.status === 1) {
// 如果正在录音
modalRef.value?.showModal({
title: "提示",
content: "你当前有未录制完成的音频,确认关闭?",
showCancel: true,
cancelStyle: {
color: "#aaa",
},
confirm: () => {
recordingStatus.status = -1;
clearInterval(timer);
recorderManager?.stop();
uni.showToast({
icon: "none",
title: "录音已取消",
});
},
});
}
};
// 计时
const countDown = () => {
timer = setInterval(() => {
if (recordingStatus.recordSeconds === recordingStatus.totalSeconds) {
recordComplate(true);
uni.hideToast();
modalRef.value?.showModal({
title: "提示",
content: "录制超时,已为你自动截取允许最长录制时长的音频!",
});
} else {
recordingStatus.recordSeconds++;
}
}, 1000);
};
/**
* 打开组件的时候
*/
const openRecordComp = () => {
recordingStatus.text = options.text;
recordingStatus.color = options.color;
recordingStatus.status = options.status;
recordingStatus.totalSeconds = options.totalSeconds;
recordingStatus.recordSeconds = options.recordSeconds;
};
</script>
<style lang="scss" scoped>
.record-panel {
.record-info {
display: flex;
flex-direction: column;
line-height: 90rpx;
}
.record-controller {
margin-top: 50rpx;
.tn-button {
text {
margin-left: 10rpx;
}
}
}
}
</style>
动画组件
仿vue原生Transition组件
注意事项:需要下载animate.css库,同时注意引入的时候不能命名为Transition进行使用,会与vue原生的Transition组件冲突,而原生组件在APP中是不支持的
查看代码
<template>
<view
v-show="delayShow"
:class="[customClass, 'animate__animated', 'transition-animate', show ? (appear ? enterClass : '') : leaveClass]"
:style="{
...customStyle,
'--animate-duration': `${duration}ms`,
'--animate-delay': `${delay}ms`,
}"
@click="$emit('click')">
<slot name="default" />
</view>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from "vue";
const delayShow = ref<boolean>(false);
let durationTime: number = 0;
let timer: NodeJS.Timeout;
const emit = defineEmits(["open", "opened", "close", "closed", "click"]);
const props = withDefaults(
defineProps<{
/**
* 是否显示动画组件,默认值为true,可不传
*/
show?: boolean;
/**
* 首次渲染时,是否显示动画效果,默认值为false,可不传
*/
appear?: boolean;
/**
* 组件显示时的动画类名,默认值为:animate__fadeIn,可不传,更多动画效果参见:https://animate.style/
*/
enterClass?: string;
/**
* 组件隐藏时的动画类名,默认值为:animate__fadeOut,可不传,更多动画效果参见:https://animate.style/
*/
leaveClass?: string;
/**
* 组件显示或隐藏时动画持续时间,默认值为500,单位毫秒,可不传
*/
duration?: number;
/**
* 组件显示或隐藏时的延迟时间,即多少秒之后组件再显示或隐藏,默认值为0,单位毫秒,可不传
*/
delay?: number;
/**
* 组件的内联样式,默认值为 {},可不传
*/
customStyle?: object;
/**
* 组件的类名,默认值为 "",可不传
*/
customClass?: string;
}>(),
{
show: true,
appear: false,
enterClass: "animate__fadeIn",
leaveClass: "animate__fadeOut",
duration: 500,
delay: 0,
customStyle: () => {
return {};
},
customClass: "",
}
);
const show = ref<boolean>(props.show);
watch(
() => props.show,
nV => {
show.value = nV;
}
);
watch(
show,
nV => {
if (timer) clearTimeout(timer);
if (nV) {
delayShow.value = nV;
emit("open");
timer = setTimeout(
() => {
emit("opened");
},
props.enterClass ? props.duration + props.delay : 0
);
} else {
emit("close");
timer = setTimeout(
() => {
delayShow.value = nV;
emit("closed");
},
props.leaveClass ? props.duration + props.delay : 0
);
}
},
{
immediate: true,
}
);
watch(
[() => props.duration, () => props.delay],
nV => {
durationTime = nV[0] + nV[1];
},
{
immediate: true,
}
);
const toggleComponent = async (callback: Function): Promise<void> => {
show.value = false;
await nextTick();
let _timer: NodeJS.Timeout = setTimeout(() => {
callback();
show.value = true;
clearTimeout(_timer);
}, durationTime);
};
defineExpose({
/**
* 暴露toggleComponent函数,通过传callback函数达到异步改变插槽内容的目的,即会等到动画结束后再改变插槽的内容
*/
toggleComponent,
});
</script>
<style lang="scss" scoped>
.transition-animate {
height: max-content;
}
</style>