This commit is contained in:
@@ -33,8 +33,14 @@
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div v-if="hasCardInfo" class="operation-button-group">
|
||||
<ElButton v-permission="'asset_info:sync'" @click="handleRefresh" type="primary" :icon="Refresh" :disabled="refreshDisabled">
|
||||
同步
|
||||
<ElButton
|
||||
v-permission="'asset_info:sync'"
|
||||
@click="handleRefresh"
|
||||
type="primary"
|
||||
:icon="Refresh"
|
||||
:disabled="refreshDisabled"
|
||||
>
|
||||
{{ refreshButtonText }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElCard, ElInput, ElButton } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
@@ -57,13 +63,19 @@
|
||||
interface Props {
|
||||
hasCardInfo?: boolean
|
||||
refreshDisabled?: boolean
|
||||
refreshRemainingText?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
hasCardInfo: false,
|
||||
refreshDisabled: false
|
||||
refreshDisabled: false,
|
||||
refreshRemainingText: ''
|
||||
})
|
||||
|
||||
const refreshButtonText = computed(() =>
|
||||
props.refreshRemainingText ? `同步(${props.refreshRemainingText})` : '同步'
|
||||
)
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
search: [payload: { identifier: string }]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import { formatRemainingTime, FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||
import type {
|
||||
AssetBoundCard,
|
||||
AssetWalletResponse,
|
||||
@@ -183,7 +184,7 @@ export function useAssetInfo() {
|
||||
ElMessage.error(response.msg || '查询失败')
|
||||
cardInfo.value = null
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
cardInfo.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -413,7 +414,7 @@ export function useAssetInfo() {
|
||||
* 刷新资产数据(调用refresh接口后重新加载)
|
||||
* @param identifier - 资产标识符
|
||||
* @param assetType - 资产类型(card/device)
|
||||
* @returns true if refresh was successful, false if rate limited (429)
|
||||
* @returns true if refresh was successful, false if rate limited
|
||||
*/
|
||||
const refreshAsset = async (identifier: string, assetType: string): Promise<boolean> => {
|
||||
try {
|
||||
@@ -426,10 +427,12 @@ export function useAssetInfo() {
|
||||
}
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
// Check if it's a 429 rate limit error
|
||||
if (error instanceof FrontendRateLimitError) {
|
||||
ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`)
|
||||
return false
|
||||
}
|
||||
if (error?.response?.status === 429) {
|
||||
ElMessage.warning('操作过于频繁,请30秒后再试')
|
||||
ElMessage.warning('操作过于频繁,请5分钟后再试')
|
||||
return false
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { AssetService, DeviceService } from '@/api/modules'
|
||||
import { formatRemainingTime, FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||
|
||||
/**
|
||||
* Asset operations composable
|
||||
@@ -38,6 +39,10 @@ export function useAssetOperations(cardInfo: Ref<any>, refreshAssetFn?: () => Pr
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
if (error instanceof FrontendRateLimitError) {
|
||||
ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`)
|
||||
return
|
||||
}
|
||||
console.error('启用失败:', error)
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
ref="assetSearchCardRef"
|
||||
:has-card-info="!!cardInfo"
|
||||
:refresh-disabled="refreshDisabled"
|
||||
:refresh-remaining-text="refreshRemainingText"
|
||||
@search="handleSearch"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
@@ -167,11 +168,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElCard, ElEmpty } from 'element-plus'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
import {
|
||||
formatRemainingTime,
|
||||
FrontendRateLimitError,
|
||||
getAssetActionRateLimitState
|
||||
} from '@/utils/business/apiRateLimit'
|
||||
|
||||
// 引入子组件
|
||||
import AssetSearchCard from './components/AssetSearchCard.vue'
|
||||
@@ -311,7 +317,34 @@
|
||||
})
|
||||
|
||||
// ========== 刷新按钮状态 ==========
|
||||
const refreshDisabled = ref(false)
|
||||
const rateLimitNow = ref(Date.now())
|
||||
const refreshSubmitting = ref(false)
|
||||
let rateLimitTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
const refreshRateLimitState = computed(() => {
|
||||
const identifier = cardInfo.value?.identifier
|
||||
if (!identifier) {
|
||||
return {
|
||||
limited: false,
|
||||
remainingMs: 0,
|
||||
retryAt: 0
|
||||
}
|
||||
}
|
||||
return getAssetActionRateLimitState('refresh', identifier, rateLimitNow.value)
|
||||
})
|
||||
|
||||
const refreshDisabled = computed(
|
||||
() => refreshSubmitting.value || refreshRateLimitState.value.limited
|
||||
)
|
||||
const refreshRemainingText = computed(() =>
|
||||
refreshRateLimitState.value.limited
|
||||
? formatRemainingTime(refreshRateLimitState.value.remainingMs)
|
||||
: ''
|
||||
)
|
||||
|
||||
const updateRateLimitNow = () => {
|
||||
rateLimitNow.value = Date.now()
|
||||
}
|
||||
|
||||
// ========== 事件处理 ==========
|
||||
|
||||
@@ -351,14 +384,14 @@
|
||||
* 处理刷新
|
||||
*/
|
||||
const handleRefresh = async () => {
|
||||
if (!cardInfo.value) return
|
||||
const success = await refreshAsset(cardInfo.value.identifier, cardInfo.value.asset_type)
|
||||
if (!success) {
|
||||
// 如果刷新失败(429错误),禁用刷新按钮30秒
|
||||
refreshDisabled.value = true
|
||||
setTimeout(() => {
|
||||
refreshDisabled.value = false
|
||||
}, 30000)
|
||||
if (!cardInfo.value || refreshDisabled.value) return
|
||||
|
||||
try {
|
||||
refreshSubmitting.value = true
|
||||
await refreshAsset(cardInfo.value.identifier, cardInfo.value.asset_type)
|
||||
} finally {
|
||||
refreshSubmitting.value = false
|
||||
updateRateLimitNow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,6 +557,10 @@
|
||||
if (cardInfo.value)
|
||||
await loadRealtimeStatus(cardInfo.value.identifier, cardInfo.value.asset_type)
|
||||
} catch (error: any) {
|
||||
if (error instanceof FrontendRateLimitError) {
|
||||
ElMessage.warning(`操作过于频繁,请${formatRemainingTime(error.remainingMs)}后再试`)
|
||||
return
|
||||
}
|
||||
console.error('启用绑定卡失败:', error)
|
||||
}
|
||||
}
|
||||
@@ -653,9 +690,16 @@
|
||||
* URL参数自动加载
|
||||
*/
|
||||
onMounted(() => {
|
||||
rateLimitTimer = setInterval(updateRateLimitNow, 1000)
|
||||
autoLoadFromQuery()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (rateLimitTimer) {
|
||||
clearInterval(rateLimitTimer)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听路由查询参数变化
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user