fix(ts): 补全音乐播放器进度交互并修复低风险类型错误

- 音乐列表改用 typed provide/inject + MusicSong,audioBar 接真实 audio 状态(进度/时长/seek/换源重载)
- 修复 SocialLogin 验证码因布尔误判而从不显示
- getBoolDictOptions 返回类型收窄为 boolean,:key 统一 String()
- CRM/售后 tab.paneName 判空 + String() 后写入查询参数
- demo03 子表入参字段名对齐接口(demo03courses/demo03grade)等单点修复

ts:check 829 → 795,无新增类型错误
pull/885/MERGE
YunaiV 2026-06-20 09:56:29 -07:00
parent a57df0b2de
commit 0970806dca
30 changed files with 148 additions and 58 deletions

View File

@ -28,6 +28,10 @@ export interface StringDictDataType extends DictDataType {
value: string
}
export interface BooleanDictDataType extends DictDataType {
value: boolean
}
export const getDictOptions = (dictType: string) => {
return dictStore.getDictByType(dictType) || []
}
@ -62,8 +66,8 @@ export const getStrDictOptions = (dictType: string) => {
return dictOption
}
export const getBoolDictOptions = (dictType: string) => {
const dictOption: DictDataType[] = []
export const getBoolDictOptions = (dictType: string): BooleanDictDataType[] => {
const dictOption: BooleanDictDataType[] = []
const dictOptions: DictDataType[] = getDictOptions(dictType)
dictOptions.forEach((dict: DictDataType) => {
dictOption.push({

View File

@ -133,7 +133,7 @@
</el-form-item>
</el-col>
<Verify
v-if="loginData.captchaEnable === 'true'"
v-if="loginData.captchaEnable"
ref="verify"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"

View File

@ -29,7 +29,7 @@
<el-select v-model="queryParams.status" placeholder="请选择平台" clearable class="!w-240px">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
:key="dict.value"
:key="String(dict.value)"
:label="dict.label"
:value="dict.value"
/>
@ -59,7 +59,7 @@
>
<el-option
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:label="dict.label"
:value="dict.value"
/>

View File

@ -51,7 +51,7 @@
<el-select v-model="formData.mcpClientNames" placeholder="请选择 MCP" clearable multiple>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.AI_MCP_CLIENT_NAME)"
:key="dict.value"
:key="String(dict.value)"
:label="dict.label"
:value="dict.value"
/>
@ -61,7 +61,7 @@
<el-radio-group v-model="formData.publicStatus">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}

View File

@ -37,7 +37,7 @@
>
<el-option
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:label="dict.label"
:value="dict.value"
/>

View File

@ -9,8 +9,8 @@
class="w-[45px]"
/>
<div>
<div>{{ currentSong.name }}</div>
<div class="text-[12px] text-gray-400">{{ currentSong.singer }}</div>
<div>{{ currentSong.title || '暂无音乐' }}</div>
<div class="text-[12px] text-gray-400">{{ currentSong.singer || currentSong.desc }}</div>
</div>
</div>
@ -26,19 +26,26 @@
<Icon icon="majesticons:next-circle" :size="20" class="text-gray-300 cursor-pointer" />
<div class="flex gap-[16px] items-center">
<span>{{ audioProps.currentTime }}</span>
<el-slider v-model="audioProgress" color="#409eff" class="w-[160px!important]" />
<el-slider
v-model="audioProgress"
:max="audioDuration"
color="#409eff"
class="w-[160px!important]"
@change="handleProgressChange"
/>
<span>{{ audioProps.duration }}</span>
</div>
<!-- 音频 -->
<audio
v-bind="audioProps"
:src="currentAudioUrl"
:autoplay="audioProps.autoplay"
:muted="audioProps.muted"
ref="audioRef"
controls
v-show="!audioProps"
@timeupdate="audioTimeUpdate"
>
<source :src="audioUrl" />
</audio>
@loadedmetadata="audioLoadedMetadata"
></audio>
</div>
<!-- 音量控制器 -->
@ -55,15 +62,17 @@
</template>
<script lang="ts" setup>
import { formatPast } from '@/utils/formatTime'
import audioUrl from '@/assets/audio/response.mp3'
import { currentSongKey, type MusicSong } from '../types'
defineOptions({ name: 'Index' })
const currentSong = inject('currentSong', {})
const currentSong = inject(currentSongKey, ref<MusicSong>({}))
const currentAudioUrl = computed(() => currentSong.value.audioUrl || audioUrl)
const audioRef = ref<Nullable<HTMLAudioElement>>(null)
const audioProgress = ref(0)
const audioDuration = ref(0)
// https://www.runoob.com/tags/ref-av-dom.html
const audioProps = reactive({
autoplay: true,
@ -74,6 +83,15 @@ const audioProps = reactive({
volume: 50
})
const formatAudioTime = (seconds: number) => {
if (!Number.isFinite(seconds)) {
return '00:00'
}
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.floor(seconds % 60)
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
}
function toggleStatus(type: string) {
audioProps[type] = !audioProps[type]
if (type === 'paused' && audioRef.value) {
@ -86,7 +104,38 @@ function toggleStatus(type: string) {
}
//
function audioTimeUpdate(args) {
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss')
function audioTimeUpdate() {
if (!audioRef.value) {
return
}
audioProgress.value = audioRef.value.currentTime
audioProps.currentTime = formatAudioTime(audioRef.value.currentTime)
}
function audioLoadedMetadata() {
if (!audioRef.value) {
return
}
audioDuration.value = audioRef.value.duration
audioProps.duration = formatAudioTime(audioRef.value.duration)
}
function handleProgressChange(value: number | number[]) {
if (!audioRef.value || Array.isArray(value)) {
return
}
audioRef.value.currentTime = value
audioProgress.value = value
audioProps.currentTime = formatAudioTime(value)
}
watch(currentAudioUrl, () => {
audioProgress.value = 0
audioDuration.value = 0
audioProps.currentTime = '00:00'
audioProps.duration = '00:00'
nextTick(() => {
audioRef.value?.load()
})
})
</script>

View File

@ -33,6 +33,7 @@
import songCard from './songCard/index.vue'
import songInfo from './songInfo/index.vue'
import audioBar from './audioBar/index.vue'
import { currentSongKey, type MusicSong } from './types'
defineOptions({ name: 'Index' })
@ -40,12 +41,12 @@ const currentType = ref('mine')
// loading
const loading = ref(false)
//
const currentSong = ref({})
const currentSong = ref<MusicSong>({})
const mySongList = ref<Recordable[]>([])
const squareSongList = ref<Recordable[]>([])
const mySongList = ref<MusicSong[]>([])
const squareSongList = ref<MusicSong[]>([])
provide('currentSong', currentSong)
provide(currentSongKey, currentSong)
/*
*@Description: 调接口生成音乐列表
@ -86,7 +87,7 @@ function generateMusic(formData: Recordable) {
*@MethodAuthor: xiaohong
*@Date: 2024-07-19 11:22:33
*/
function setCurrentSong(music: Recordable) {
function setCurrentSong(music: MusicSong) {
currentSong.value = music
}

View File

@ -25,18 +25,17 @@
</template>
<script lang="ts" setup>
import { currentSongKey, type MusicSong } from '../types'
defineOptions({ name: 'Index' })
defineProps({
songInfo: {
type: Object,
default: () => ({})
}
withDefaults(defineProps<{ songInfo?: MusicSong }>(), {
songInfo: () => ({})
})
const emits = defineEmits(['play'])
const currentSong = inject('currentSong', {})
const currentSong = inject(currentSongKey, ref<MusicSong>({}))
function playSong() {
emits('play')

View File

@ -14,7 +14,9 @@
</template>
<script lang="ts" setup>
import { currentSongKey, type MusicSong } from '../types'
defineOptions({ name: 'Index' })
const currentSong = inject('currentSong', {})
const currentSong = inject(currentSongKey, ref<MusicSong>({}))
</script>

View File

@ -0,0 +1,14 @@
import type { InjectionKey, Ref } from 'vue'
export interface MusicSong {
id?: number
title?: string
singer?: string
imageUrl?: string
audioUrl?: string
desc?: string
date?: string
lyric?: string
}
export const currentSongKey: InjectionKey<Ref<MusicSong>> = Symbol('currentSong')

View File

@ -43,7 +43,7 @@
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.AI_MUSIC_STATUS)"
:key="dict.value"
:key="String(dict.value)"
:label="dict.label"
:value="dict.value"
/>
@ -84,7 +84,7 @@
>
<el-option
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:label="dict.label"
:value="dict.value"
/>

View File

@ -62,7 +62,7 @@
<el-radio-group v-model="modelData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value as string"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}

View File

@ -219,7 +219,10 @@ const resetQuery = () => {
/** tab 切换 */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.sceneType = tab.paneName
if (tab.paneName === undefined) {
return
}
queryParams.sceneType = String(tab.paneName)
handleQuery()
}

View File

@ -219,7 +219,10 @@ const resetQuery = () => {
/** tab 切换 */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.sceneType = tab.paneName
if (tab.paneName === undefined) {
return
}
queryParams.sceneType = String(tab.paneName)
handleQuery()
}

View File

@ -89,7 +89,7 @@
<el-radio-group v-model="formData.master">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}

View File

@ -275,7 +275,10 @@ const resetQuery = () => {
/** tab 切换 */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.sceneType = tab.paneName
if (tab.paneName === undefined) {
return
}
queryParams.sceneType = String(tab.paneName)
handleQuery()
}

View File

@ -294,7 +294,10 @@ const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
/** tab 切换 */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.sceneType = tab.paneName
if (tab.paneName === undefined) {
return
}
queryParams.sceneType = String(tab.paneName)
handleQuery()
}

View File

@ -235,7 +235,10 @@ const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
/** tab 切换 */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.sceneType = tab.paneName
if (tab.paneName === undefined) {
return
}
queryParams.sceneType = String(tab.paneName)
handleQuery()
}

View File

@ -247,7 +247,10 @@ const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
/** tab 切换 */
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.sceneType = tab.paneName
if (tab.paneName === undefined) {
return
}
queryParams.sceneType = String(tab.paneName)
handleQuery()
}

View File

@ -29,7 +29,7 @@
</Dialog>
</template>
<script setup lang="ts">
import { ProductUnitApi } from '@/api/erp/product/unit'
import { ProductUnitApi, type ProductUnitVO } from '@/api/erp/product/unit'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
@ -80,7 +80,7 @@ const submitForm = async () => {
//
formLoading.value = true
try {
const data = formData.value as unknown as ProductUnitApi.ProductUnitVO
const data = formData.value as unknown as ProductUnitVO
if (formType.value === 'create') {
await ProductUnitApi.createProductUnit(data)
message.success(t('common.createSuccess'))

View File

@ -23,7 +23,7 @@
<el-radio-group v-model="formData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value as string"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}

View File

@ -125,8 +125,8 @@ const submitForm = async () => {
try {
const data = formData.value as unknown as Demo03Student
//
data.demo03Courses = demo03CourseFormRef.value.getData()
data.demo03Grade = demo03GradeFormRef.value.getData()
data.demo03courses = demo03CourseFormRef.value.getData()
data.demo03grade = demo03GradeFormRef.value.getData()
if (formType.value === 'create') {
await Demo03StudentApi.createDemo03Student(data)
message.success(t('common.createSuccess'))

View File

@ -125,8 +125,8 @@ const submitForm = async () => {
try {
const data = formData.value as unknown as Demo03Student
//
data.demo03Courses = demo03CourseFormRef.value.getData()
data.demo03Grade = demo03GradeFormRef.value.getData()
data.demo03courses = demo03CourseFormRef.value.getData()
data.demo03grade = demo03GradeFormRef.value.getData()
if (formType.value === 'create') {
await Demo03StudentApi.createDemo03Student(data)
message.success(t('common.createSuccess'))

View File

@ -50,7 +50,7 @@
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}
@ -63,7 +63,7 @@
<el-radio-group v-model="formData.recommendHot">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}
@ -76,7 +76,7 @@
<el-radio-group v-model="formData.recommendBanner">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}

View File

@ -246,7 +246,7 @@ const handleEmitChange = () => {
/** 确认选择时的触发事件 */
const emits = defineEmits<{
(e: CHANGE_EVENT, v: PointActivityVO | PointActivityVO[] | any): void
(e: typeof CHANGE_EVENT, v: PointActivityVO | PointActivityVO[] | any): void
}>()
/** 全选/全不选 */

View File

@ -239,7 +239,10 @@ const resetQuery = () => {
/** tab 切换 */
const tabClick = async (tab: TabsPaneContext) => {
queryParams.status = tab.paneName
if (tab.paneName === undefined) {
return
}
queryParams.status = String(tab.paneName)
await getList()
}

View File

@ -18,7 +18,7 @@
<el-radio-group v-model="formData.primaryFlag">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}

View File

@ -106,7 +106,7 @@ const onAccountChanged = (id: number) => {
}
//
const onBeforeDialogClose = async (onDone: () => {}) => {
const onBeforeDialogClose = async (onDone: () => void) => {
try {
await message.confirm('修改内容可能还未保存,确定关闭吗?')
onDone()

View File

@ -36,7 +36,7 @@
<el-radio-group v-model="formData.sslEnable">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}
@ -47,7 +47,7 @@
<el-radio-group v-model="formData.starttlsEnable">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}

View File

@ -19,7 +19,7 @@
>
<el-option
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="String(dict.value)"
:label="dict.label"
:value="dict.value"
/>