Compare commits

..

7 Commits

Author SHA1 Message Date
60af90486c fix: 修复hub连接 2025-04-26 06:25:47 +08:00
1bf2a40516 并发控制 2025-04-26 06:19:21 +08:00
1276cc0381 feat: 优化 ClientAutoAction 组件的状态显示
- 修改了状态标签的样式
2025-04-26 06:12:24 +08:00
08d8ca577d feat: 优化 SignalR 连接管理,增强错误处理和重连机制
- 在 useWebFetcher 中添加 SignalR 连接停止和重置逻辑。
- 修改连接关闭时的错误日志,增加重连提示。
- 移除不必要的状态标记,简化重连流程。
2025-04-26 06:04:09 +08:00
2a67d20e66 1 2025-04-26 05:55:36 +08:00
0beb49e589 feat: 更新 BiliAuthView 组件提示信息
- 修改了登录链接提示信息,增加了在其他地方登录的说明
2025-04-26 05:33:34 +08:00
3a73801340 feat: 更新用户中心组件,增强数据加载和刷新功能
- 在 PointOrderView 和 PointUserHistoryView 中新增数据加载完成事件,优化数据获取逻辑。
- 在 PointUserLayout 中实现标签页数据加载状态管理,提升用户体验。
- 为各组件添加重置方法,支持父组件调用,增强灵活性。
- 更新 PointUserSettings 组件,提供重置功能,确保状态管理一致性。
2025-04-26 05:24:58 +08:00
7 changed files with 275 additions and 73 deletions

View File

@@ -510,23 +510,15 @@ function confirmTest() {
:name="type"
>
<template #tab>
<NSpace
align="center"
size="small"
inline
>
{{ label }}
<span
:style="{
color: enabledTriggerTypes && enabledTriggerTypes[type] ? '#18a058' : '#d03050',
fontSize: '14px', // Adjust size as needed
verticalAlign: 'middle'
fontWeight: 'medium'
}"
:title="enabledTriggerTypes && enabledTriggerTypes[type] ? '已启用' : '已禁用'"
>
{{ label }}
</span>
</NSpace>
</template>
<NSpace vertical>
<NSpace

View File

@@ -208,15 +208,22 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
}
}
let isConnectingSignalR = false;
/**
* 连接 SignalR 服务器
*/
async function connectSignalR() {
async function connectSignalR(): Promise<boolean> {
if (isConnectingSignalR) {
return false;
}
isConnectingSignalR = true;
if (signalRClient.value && signalRClient.value.state !== signalR.HubConnectionState.Disconnected) {
console.log(prefix.value + "SignalR 已连接或正在连接");
return true;
}
signalRClient.value?.stop();
signalRClient.value = undefined;
signalRConnectionId.value = undefined;
console.log(prefix.value + '正在连接到 vtsuru 服务器...');
const connection = new signalR.HubConnectionBuilder()
.withUrl(BASE_HUB_URL + 'web-fetcher?token=' + (route.query.token ?? account.value.token), { // 使用 account.token
@@ -232,6 +239,19 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
.withHubProtocol(new msgpack.MessagePackHubProtocol()) // 使用 MessagePack 协议
.build();
connection.on('Disconnect', (reason: unknown) => {
console.log(prefix.value + '被服务器断开连接: ' + reason);
disconnectedByServer = true; // 标记是服务器主动断开
window.$message.error(`被服务器要求断开连接: ${reason}`);
});
// --- 尝试启动连接 ---
try {
await connection.start();
signalRConnectionId.value = connection.connectionId ?? undefined; // 保存连接ID
signalRId.value = await sendSelfInfo(connection); // 发送客户端信息
await connection.send('Finished'); // 通知服务器已准备好
signalRClient.value = connection; // 保存实例
// --- SignalR 事件监听 ---
connection.onreconnecting(error => {
console.log(prefix.value + `与服务器断开,正在尝试重连... ${error?.message || ''}`);
@@ -250,10 +270,18 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
connection.onclose(async (error) => {
// 只有在不是由 Stop() 或服务器明确要求断开时才记录错误并尝试独立重连(虽然 withAutomaticReconnect 应该处理)
if (state.value !== 'disconnected' && !disconnectedByServer) {
console.error(prefix.value + `与服务器连接关闭: ${error?.message || '未知原因'}. 自动重连将处理.`);
state.value = 'connecting'; // 标记为连接中,等待自动重连
console.error(prefix.value + `与服务器连接关闭: ${error?.message || '未知原因'}. 30秒后将自动重启`);
//state.value = 'connecting'; // 标记为连接中,等待自动重连
//signalRConnectionId.value = undefined;
//await connection.start();
// 停止 SignalR 连接
signalRClient.value?.stop();
signalRClient.value = undefined;
signalRConnectionId.value = undefined;
await connection.start();
setTimeout(() => {
console.log(prefix.value + '尝试重启...');
connectSignalR(); // 30秒后尝试重启
}, 30 * 1000); // 30秒后自动重启
} else if (disconnectedByServer) {
console.log(prefix.value + `连接已被服务器关闭.`);
//Stop(); // 服务器要求断开,则彻底停止
@@ -261,26 +289,9 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
console.log(prefix.value + `连接已手动关闭.`);
}
});
connection.on('Disconnect', (reason: unknown) => {
console.log(prefix.value + '被服务器断开连接: ' + reason);
disconnectedByServer = true; // 标记是服务器主动断开
window.$message.error(`被服务器要求断开连接: ${reason}, 为保证可用性, 30秒后将自动重启`);
//Stop(); // 服务器要求断开,调用 Stop 清理所有资源
setTimeout(() => {
console.log(prefix.value + '尝试重启...');
connectSignalR(); // 30秒后尝试重启
}, 30 * 1000); // 30秒后自动重启
});
connection.on('Request', async (url: string, method: string, body: string, useCookie: boolean) => onRequest(url, method, body, useCookie));
connection.on('Notification', (type: string, data: any) => { onReceivedNotification(type, data); });
// --- 尝试启动连接 ---
try {
await connection.start();
signalRConnectionId.value = connection.connectionId ?? undefined; // 保存连接ID
signalRId.value = await sendSelfInfo(connection); // 发送客户端信息
await connection.send('Finished'); // 通知服务器已准备好
signalRClient.value = connection; // 保存实例
console.log(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + signalRId.value); // 调试输出连接状态
// state.value = 'connected'; // 状态将在 Start 函数末尾统一设置
return true;
@@ -290,6 +301,8 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
signalRClient.value = undefined;
// state.value = 'disconnected'; // 保持 connecting 或由 Start 控制
return false;
} finally {
isConnectingSignalR = false;
}
}
async function sendSelfInfo(client: signalR.HubConnection) {

View File

@@ -243,7 +243,9 @@ onMounted(async () => {
style="width: 100%"
>
<NAlert type="success">
你已完成验证! 请妥善保存你的登陆链接, 请勿让其他人获取. 丢失后可以再次通过认证流程获得. 把这个链接复制到浏览器打开即可登录
你已完成验证! 请妥善保存你的登陆链接, 请勿让其他人获取. 丢失后可以再次通过认证流程获得.
<br>
要在其他地方登陆, 或者需要重新登陆的话把这个链接复制到浏览器地址栏打开即可
</NAlert>
<NText> 你的登陆链接为: </NText>
<NInputGroup>

View File

@@ -3,7 +3,7 @@ import { ResponsePointOrder2UserModel } from '@/api/api-models'
import PointOrderCard from '@/components/manage/PointOrderCard.vue'
import { POINT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore'
import { NEmpty, NSpin, useMessage } from 'naive-ui'
import { NButton, NEmpty, NFlex, NSpin, useMessage } from 'naive-ui'
import { onMounted, ref } from 'vue'
const message = useMessage()
@@ -12,11 +12,17 @@ const useAuth = useAuthStore()
const orders = ref<ResponsePointOrder2UserModel[]>([])
const isLoading = ref(false)
// 定义加载完成的事件
const emit = defineEmits(['dataLoaded'])
async function getOrders() {
try {
isLoading.value = true
const data = await useAuth.QueryBiliAuthGetAPI<ResponsePointOrder2UserModel[]>(POINT_API_URL + 'user/get-orders')
if (data.code == 200) {
orders.value = data.data
// 触发数据加载完成事件
emit('dataLoaded')
return data.data
} else {
message.error('获取订单失败: ' + data.message)
@@ -30,6 +36,17 @@ async function getOrders() {
return []
}
// 提供给父组件调用的重置方法
function reset() {
orders.value = []
}
// 暴露方法给父组件
defineExpose({
getOrders,
reset
})
onMounted(async () => {
orders.value = await getOrders()
})
@@ -37,6 +54,9 @@ onMounted(async () => {
<template>
<NSpin :show="isLoading">
<NFlex justify="end" style="margin-bottom: 10px">
<NButton size="small" type="primary" @click="getOrders">刷新订单</NButton>
</NFlex>
<NEmpty
v-if="orders.length == 0"
description="暂无订单"

View File

@@ -3,7 +3,7 @@ import { ResponsePointHisrotyModel } from '@/api/api-models'
import PointHistoryCard from '@/components/manage/PointHistoryCard.vue'
import { POINT_API_URL } from '@/data/constants'
import { useAuthStore } from '@/store/useAuthStore'
import { NSpin, useMessage } from 'naive-ui'
import { NButton, NEmpty, NFlex, NSpin, useMessage } from 'naive-ui'
import { onMounted, ref } from 'vue'
const message = useMessage()
@@ -12,6 +12,9 @@ const isLoading = ref(false)
const history = ref<ResponsePointHisrotyModel[]>([])
// 定义加载完成的事件
const emit = defineEmits(['dataLoaded'])
// 获取积分历史记录
async function getHistories() {
try {
@@ -19,6 +22,9 @@ async function getHistories() {
const data = await useAuth.QueryBiliAuthGetAPI<ResponsePointHisrotyModel[]>(POINT_API_URL + 'user/get-histories')
if (data.code == 200) {
console.log('[point] 已获取积分历史')
history.value = data.data
// 触发数据加载完成事件
emit('dataLoaded')
return data.data
} else {
message.error('获取积分历史失败: ' + data.message)
@@ -33,6 +39,17 @@ async function getHistories() {
return []
}
// 提供给父组件调用的重置方法
function reset() {
history.value = []
}
// 暴露方法给父组件
defineExpose({
getHistories,
reset
})
onMounted(async () => {
history.value = await getHistories()
})
@@ -40,7 +57,15 @@ onMounted(async () => {
<template>
<NSpin :show="isLoading">
<NFlex justify="end" style="margin-bottom: 10px">
<NButton size="small" type="primary" @click="getHistories">刷新记录</NButton>
</NFlex>
<NEmpty
v-if="history.length == 0"
description="暂无积分记录"
/>
<PointHistoryCard
v-else
:histories="history"
/>
</NSpin>

View File

@@ -22,14 +22,32 @@ import {
NTabPane,
NTabs,
NText,
NTag,
useMessage,
} from 'naive-ui'
import { computed, h, onMounted, ref } from 'vue'
import { computed, h, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PointOrderView from './PointOrderView.vue'
import PointUserHistoryView from './PointUserHistoryView.vue'
import PointUserSettings from './PointUserSettings.vue'
// 定义组件接口
interface ComponentWithReset {
reset: () => void;
}
interface OrderViewInstance extends ComponentWithReset {
getOrders: () => Promise<any>;
}
interface HistoryViewInstance extends ComponentWithReset {
getHistories: () => Promise<any>;
}
interface SettingsViewInstance extends ComponentWithReset {
// 设置组件可能需要的方法
}
const useAuth = useAuthStore()
const message = useMessage()
const realHash = useRouteHash('points', {
@@ -48,6 +66,19 @@ const router = useRouter()
const biliAuth = computed(() => useAuth.biliAuth)
const isLoading = ref(false)
const points = ref<{ owner: UserInfo; points: number }[]>([])
const isFirstMounted = ref(true)
// 分别定义各组件引用,使用正确的类型
const orderViewRef = ref<OrderViewInstance | null>(null)
const historyViewRef = ref<HistoryViewInstance | null>(null)
const settingsViewRef = ref<SettingsViewInstance | null>(null)
// 跟踪各标签页数据是否已加载
const tabDataLoaded = ref({
points: false,
orders: false,
histories: false,
settings: true // 设置页面不需要加载数据
})
const pointColumn = [
{
@@ -89,6 +120,7 @@ async function getAllPoints() {
if (data.code == 200) {
console.log('[point] 已获取积分')
points.value = data.data
tabDataLoaded.value.points = true
return data.data
}
} catch (err) {
@@ -99,21 +131,81 @@ async function getAllPoints() {
}
return []
}
// 重置所有数据
function resetData() {
points.value = []
isFirstMounted.value = true
// 重置数据加载状态
Object.keys(tabDataLoaded.value).forEach(key => {
tabDataLoaded.value[key as keyof typeof tabDataLoaded.value] = false
})
tabDataLoaded.value.settings = true // 设置页不需要加载数据
// 重置所有子组件的数据
orderViewRef.value?.reset?.()
historyViewRef.value?.reset?.()
settingsViewRef.value?.reset?.()
}
function switchAuth(token: string) {
if (token == useAuth.biliToken) {
message.info('当前正在使用该账号')
return
}
resetData()
useAuth.setCurrentAuth(token)
message.success('已选择账号')
}
let isFirstMounted = true
function onAllPointPaneMounted() {
if (!isFirstMounted) return
isFirstMounted = false
if (!isFirstMounted.value) return
isFirstMounted.value = false
getAllPoints()
}
// 处理选项卡切换
function onTabChange(tabName: string) {
// 只在数据未加载时刷新
switch(tabName) {
case 'points':
if (!tabDataLoaded.value.points) {
getAllPoints()
}
break
case 'orders':
if (!tabDataLoaded.value.orders && orderViewRef.value) {
orderViewRef.value.getOrders()
tabDataLoaded.value.orders = true
}
break
case 'histories':
if (!tabDataLoaded.value.histories && historyViewRef.value) {
historyViewRef.value.getHistories()
tabDataLoaded.value.histories = true
}
break
}
}
// 监听 biliToken 变化
watch(() => useAuth.biliToken, (newToken) => {
if (newToken) {
resetData()
getAllPoints()
}
})
// 手动刷新当前标签页数据
function refreshCurrentTab() {
if (!hash.value) return
// 将当前标签设为未加载状态
tabDataLoaded.value[hash.value as keyof typeof tabDataLoaded.value] = false
// 触发刷新
onTabChange(hash.value)
}
onMounted(async () => {
const route = useRoute()
if (route.query.auth) {
@@ -205,10 +297,28 @@ onMounted(async () => {
style="padding: 10px"
bordered
>
<NFlex justify="center">
<NFlex
justify="space-between"
align="center"
>
<NButton
type="primary"
secondary
@click="$router.back()"
>
返回
</NButton>
<NText style="font-size: 24px">
认证用户个人中心
</NText>
<NButton
size="small"
type="primary"
:disabled="!hash"
@click="refreshCurrentTab"
>
刷新数据
</NButton>
</NFlex>
</NLayoutHeader>
<NLayoutContent content-style="padding: 24px;">
@@ -236,6 +346,20 @@ onMounted(async () => {
<NDescriptionsItem label="OpenId">
{{ biliAuth.openId }}
</NDescriptionsItem>
<NDescriptionsItem label="状态">
<NTag
v-if="biliAuth.id > 0"
type="success"
>
已认证
</NTag>
<NTag
v-else
type="error"
>
未认证
</NTag>
</NDescriptionsItem>
</NDescriptions>
</NCard>
<NDivider />
@@ -244,6 +368,7 @@ onMounted(async () => {
v-model:value="hash"
default-value="points"
animated
@update:value="onTabChange"
>
<NTabPane
name="points"
@@ -252,15 +377,21 @@ onMounted(async () => {
@vue:mounted="onAllPointPaneMounted"
>
<NDivider style="margin-top: 10px" />
<NButton
<NFlex
justify="end"
style="margin-bottom: 10px"
>
<NButton
size="small"
type="primary"
@click="getAllPoints()"
@click="() => {
tabDataLoaded.points = false;
getAllPoints();
}"
>
刷新
刷新积分
</NButton>
<NDivider />
</NFlex>
<NFlex justify="center">
<NDataTable
:loading="isLoading"
@@ -278,7 +409,10 @@ onMounted(async () => {
display-directive="show:lazy"
>
<NDivider style="margin-top: 10px" />
<PointOrderView />
<PointOrderView
ref="orderViewRef"
@data-loaded="tabDataLoaded.orders = true"
/>
</NTabPane>
<NTabPane
name="histories"
@@ -286,7 +420,10 @@ onMounted(async () => {
display-directive="show:lazy"
>
<NDivider style="margin-top: 10px" />
<PointUserHistoryView />
<PointUserHistoryView
ref="historyViewRef"
@data-loaded="tabDataLoaded.histories = true"
/>
</NTabPane>
<NTabPane
name="settings"
@@ -294,7 +431,7 @@ onMounted(async () => {
display-directive="show:lazy"
>
<NDivider style="margin-top: 10px" />
<PointUserSettings />
<PointUserSettings ref="settingsViewRef" />
</NTabPane>
</NTabs>
</div>

View File

@@ -252,6 +252,19 @@ function switchAuth(token: string) {
function logout() {
useAuth.logout()
}
// 提供给父组件调用的重置方法
function reset() {
// 重置表单数据或其他状态
currentAddress.value = {} as AddressInfo
userAgree.value = false
// 可能还需要重置其他状态
}
// 暴露方法给父组件
defineExpose({
reset
})
</script>
<template>