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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,8 @@
class="w-[45px]" class="w-[45px]"
/> />
<div> <div>
<div>{{ currentSong.name }}</div> <div>{{ currentSong.title || '暂无音乐' }}</div>
<div class="text-[12px] text-gray-400">{{ currentSong.singer }}</div> <div class="text-[12px] text-gray-400">{{ currentSong.singer || currentSong.desc }}</div>
</div> </div>
</div> </div>
@ -26,19 +26,26 @@
<Icon icon="majesticons:next-circle" :size="20" class="text-gray-300 cursor-pointer" /> <Icon icon="majesticons:next-circle" :size="20" class="text-gray-300 cursor-pointer" />
<div class="flex gap-[16px] items-center"> <div class="flex gap-[16px] items-center">
<span>{{ audioProps.currentTime }}</span> <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> <span>{{ audioProps.duration }}</span>
</div> </div>
<!-- 音频 --> <!-- 音频 -->
<audio <audio
v-bind="audioProps" :src="currentAudioUrl"
:autoplay="audioProps.autoplay"
:muted="audioProps.muted"
ref="audioRef" ref="audioRef"
controls controls
v-show="!audioProps" v-show="!audioProps"
@timeupdate="audioTimeUpdate" @timeupdate="audioTimeUpdate"
> @loadedmetadata="audioLoadedMetadata"
<source :src="audioUrl" /> ></audio>
</audio>
</div> </div>
<!-- 音量控制器 --> <!-- 音量控制器 -->
@ -55,15 +62,17 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { formatPast } from '@/utils/formatTime'
import audioUrl from '@/assets/audio/response.mp3' import audioUrl from '@/assets/audio/response.mp3'
import { currentSongKey, type MusicSong } from '../types'
defineOptions({ name: 'Index' }) 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 audioRef = ref<Nullable<HTMLAudioElement>>(null)
const audioProgress = ref(0) const audioProgress = ref(0)
const audioDuration = ref(0)
// https://www.runoob.com/tags/ref-av-dom.html // https://www.runoob.com/tags/ref-av-dom.html
const audioProps = reactive({ const audioProps = reactive({
autoplay: true, autoplay: true,
@ -74,6 +83,15 @@ const audioProps = reactive({
volume: 50 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) { function toggleStatus(type: string) {
audioProps[type] = !audioProps[type] audioProps[type] = !audioProps[type]
if (type === 'paused' && audioRef.value) { if (type === 'paused' && audioRef.value) {
@ -86,7 +104,38 @@ function toggleStatus(type: string) {
} }
// //
function audioTimeUpdate(args) { function audioTimeUpdate() {
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss') 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> </script>

View File

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

View File

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

View File

@ -14,7 +14,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { currentSongKey, type MusicSong } from '../types'
defineOptions({ name: 'Index' }) defineOptions({ name: 'Index' })
const currentSong = inject('currentSong', {}) const currentSong = inject(currentSongKey, ref<MusicSong>({}))
</script> </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 <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.AI_MUSIC_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.AI_MUSIC_STATUS)"
:key="dict.value" :key="String(dict.value)"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
/> />
@ -84,7 +84,7 @@
> >
<el-option <el-option
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value" :key="String(dict.value)"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -246,7 +246,7 @@ const handleEmitChange = () => {
/** 确认选择时的触发事件 */ /** 确认选择时的触发事件 */
const emits = defineEmits<{ 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 切换 */ /** tab 切换 */
const tabClick = async (tab: TabsPaneContext) => { const tabClick = async (tab: TabsPaneContext) => {
queryParams.status = tab.paneName if (tab.paneName === undefined) {
return
}
queryParams.status = String(tab.paneName)
await getList() await getList()
} }

View File

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

View File

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

View File

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

View File

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