Files
xyzw_web_helper/src/views/DailyTasks.vue
steve 29ce1d4683 feat(DailyTasks): 进入日常页面时自动加载阵容数据- 新增 loadTeamDataWithConnection 函数,用于检查 WebSocket 连接状态并加载阵容数据
- 在页面进入时调用该函数,确保 WebSocket 连接成功后再加载阵容数据
- 增加重试机制,提高加载成功率
- 优化错误处理,提升用户体验
2025-09-03 19:04:41 +08:00

835 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="daily-tasks-page">
<!-- 页面头部 -->
<div class="page-header">
<div class="container">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">
日常任务
</h1>
<p class="page-subtitle">
管理和执行您的日常游戏任务
</p>
</div>
<div class="header-actions">
<n-button
type="primary"
size="large"
:loading="isRefreshing"
@click="refreshTasks"
>
<template #icon>
<n-icon>
<Refresh />
</n-icon>
</template>
刷新任务
</n-button>
<n-dropdown
:options="bulkActionOptions"
@select="handleBulkAction"
>
<n-button size="large">
批量操作
<template #icon>
<n-icon>
<ChevronDown />
</n-icon>
</template>
</n-button>
</n-dropdown>
</div>
</div>
</div>
</div>
<!-- 角色选择器 -->
<div class="role-selector-section">
<div class="container">
<div class="role-selector">
<span class="selector-label">选择角色</span>
<n-select
v-model:value="selectedRoleId"
:options="roleOptions"
placeholder="请选择游戏角色"
style="min-width: 200px"
@update:value="onRoleChange"
/>
<div
v-if="selectedRole"
class="role-stats"
>
<div class="stat-item">
<span class="stat-label">总任务</span>
<span class="stat-value">{{ taskStats.total }}</span>
</div>
<div class="stat-item">
<span class="stat-label">已完成</span>
<span class="stat-value">{{ taskStats.completed }}</span>
</div>
<div class="stat-item">
<span class="stat-label">进度</span>
<span class="stat-value">{{ taskStats.percentage }}%</span>
</div>
</div>
</div>
</div>
</div>
<!-- 任务筛选 -->
<div class="filter-section">
<div class="container">
<div class="filter-bar">
<n-radio-group
v-model:value="currentFilter"
@update:value="onFilterChange"
>
<n-radio-button value="all">
全部任务
</n-radio-button>
<n-radio-button value="pending">
待完成
</n-radio-button>
<n-radio-button value="completed">
已完成
</n-radio-button>
<n-radio-button value="auto">
自动执行
</n-radio-button>
</n-radio-group>
<div class="search-box">
<n-input
v-model:value="searchKeyword"
placeholder="搜索任务..."
clearable
@update:value="onSearch"
>
<template #prefix>
<n-icon>
<Search />
</n-icon>
</template>
</n-input>
</div>
</div>
</div>
</div>
<!-- 任务列表 -->
<div class="tasks-section">
<div class="container">
<div
v-if="filteredTasks.length"
class="tasks-grid"
>
<DailyTaskCard
v-for="task in filteredTasks"
:key="task.id"
:task="task"
@execute="executeTask"
@toggle-status="toggleTaskStatus"
@update:task="updateTask"
/>
</div>
<!-- 空状态 -->
<div
v-else-if="!isLoading"
class="empty-state"
>
<n-empty
description="暂无任务数据"
size="large"
>
<template #icon>
<n-icon>
<Cube />
</n-icon>
</template>
<template #extra>
<n-button
type="primary"
@click="refreshTasks"
>
刷新任务
</n-button>
</template>
</n-empty>
</div>
<!-- 加载状态 -->
<div
v-if="isLoading"
class="loading-state"
>
<n-spin size="large">
<template #description>
正在加载任务数据...
</template>
</n-spin>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage, useDialog } from 'naive-ui'
import { useTokenStore } from '@/stores/tokenStore'
import DailyTaskCard from '@/components/DailyTaskCard.vue'
import {
Refresh,
ChevronDown,
Search,
Cube
} from '@vicons/ionicons5'
const router = useRouter()
const message = useMessage()
const dialog = useDialog()
const tokenStore = useTokenStore()
// 响应式数据
const isLoading = ref(false)
const isRefreshing = ref(false)
const selectedRoleId = ref(null)
const currentFilter = ref('all')
const searchKeyword = ref('')
const tasks = ref([])
// 计算属性
const selectedRole = computed(() => {
return gameRolesStore.gameRoles.find(role => role.id === selectedRoleId.value)
})
const roleOptions = computed(() => {
return gameRolesStore.gameRoles.map(role => ({
label: `${role.name} (${role.server})`,
value: role.id
}))
})
const taskStats = computed(() => {
const total = tasks.value.length
const completed = tasks.value.filter(task => task.completed).length
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0
return { total, completed, percentage }
})
const filteredTasks = computed(() => {
let filtered = tasks.value
// 状态筛选
switch (currentFilter.value) {
case 'pending':
filtered = filtered.filter(task => !task.completed)
break
case 'completed':
filtered = filtered.filter(task => task.completed)
break
case 'auto':
filtered = filtered.filter(task => task.settings?.autoExecute)
break
}
// 关键词搜索
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
filtered = filtered.filter(task =>
task.title.toLowerCase().includes(keyword) ||
task.subtitle?.toLowerCase().includes(keyword)
)
}
return filtered
})
const bulkActionOptions = [
{
label: '执行所有待完成任务',
key: 'execute-all-pending'
},
{
label: '标记所有为已完成',
key: 'mark-all-completed'
},
{
label: '重置所有任务状态',
key: 'reset-all-tasks'
}
]
// 等待WebSocket连接并加载阵容数据
const loadTeamDataWithConnection = async (tokenId, maxRetries = 3, retryDelay = 2000) => {
console.log('每日页面进入开始检查WebSocket连接状态...')
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// 检查WebSocket连接状态
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
console.log(`${attempt}次检查WebSocket状态:`, wsStatus)
if (wsStatus !== 'connected') {
console.log('WebSocket未连接尝试建立连接...')
// 尝试建立WebSocket连接
const tokenData = tokenStore.gameTokens.find(t => t.id === tokenId)
if (tokenData && tokenData.token) {
// 触发WebSocket连接
tokenStore.createWebSocketConnection(tokenId, tokenData.token, tokenData.wsUrl)
// 等待连接建立
await new Promise(resolve => setTimeout(resolve, retryDelay))
// 再次检查连接状态
const newStatus = tokenStore.getWebSocketStatus(tokenId)
if (newStatus !== 'connected') {
if (attempt < maxRetries) {
console.log(`连接未建立,${retryDelay / 1000}秒后重试...`)
continue
} else {
throw new Error('WebSocket连接超时')
}
}
} else {
throw new Error('未找到有效的Token数据或WebSocket URL')
}
}
// WebSocket已连接开始加载阵容数据
console.log('WebSocket已连接开始加载阵容数据...')
const result = await tokenStore.sendMessageWithPromise(
tokenId,
'presetteam_getinfo',
{},
8000
)
if (result) {
// 更新到游戏数据缓存中
tokenStore.$patch((state) => {
state.gameData = { ...(state.gameData ?? {}), presetTeam: result }
})
console.log('阵容数据加载成功:', result)
message.success('阵容数据已更新')
return result
}
} catch (error) {
console.error(`${attempt}次尝试失败:`, error)
if (attempt < maxRetries) {
console.log(`${retryDelay / 1000}秒后进行第${attempt + 1}次重试...`)
await new Promise(resolve => setTimeout(resolve, retryDelay))
} else {
console.error('所有重试均失败,阵容数据加载失败')
message.warning(`阵容数据加载失败: ${error.message || '未知错误'}`)
return null
}
}
}
}
// 方法
const refreshTasks = async () => {
if (!selectedRoleId.value) {
message.warning('请先选择游戏角色')
return
}
try {
isRefreshing.value = true
isLoading.value = true
// 使用本地模拟任务数据
const mockTasks = generateMockTasks(selectedRoleId.value)
tasks.value = mockTasks
// 缓存到本地存储
localStorage.setItem(`dailyTasks_${selectedRoleId.value}`, JSON.stringify(mockTasks))
message.success('任务数据刷新成功')
} catch (error) {
console.error('刷新任务失败:', error)
message.error('本地数据生成失败')
} finally {
isRefreshing.value = false
isLoading.value = false
}
}
// 生成模拟任务数据
const generateMockTasks = (roleId) => {
const role = gameRolesStore.gameRoles.find(r => r.id === roleId)
const roleName = role?.name || '未知角色'
return [
{
id: `task_${roleId}_daily_signin`,
title: '每日签到',
subtitle: '登录游戏获取签到奖励',
icon: '/icons/ta.png',
completed: false,
canExecute: true,
progress: { current: 0, total: 1 },
reward: '金币 x100, 经验 x50',
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
settings: { autoExecute: false, delay: 0, notification: true },
details: [
{ id: 1, name: '打开游戏客户端', completed: false },
{ id: 2, name: '点击签到按钮', completed: false }
],
logs: []
},
{
id: `task_${roleId}_daily_quest`,
title: '完成日常任务',
subtitle: '完成5个日常任务获得奖励',
icon: '/icons/ta.png',
completed: false,
canExecute: true,
progress: { current: 2, total: 5 },
reward: '金币 x500, 装备碎片 x10',
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
settings: { autoExecute: true, delay: 5, notification: true },
details: [
{ id: 1, name: '击败10只怪物', completed: true },
{ id: 2, name: '收集20个材料', completed: true },
{ id: 3, name: '完成一次副本', completed: false },
{ id: 4, name: '参与公会活动', completed: false },
{ id: 5, name: '强化装备', completed: false }
],
logs: [
{ id: 1, timestamp: Date.now() - 30 * 60 * 1000, type: 'success', message: '已完成击败怪物任务' },
{ id: 2, timestamp: Date.now() - 60 * 60 * 1000, type: 'success', message: '已完成材料收集任务' }
]
},
{
id: `task_${roleId}_guild_contribution`,
title: '公会贡献',
subtitle: '为公会贡献资源获得贡献点',
icon: '/icons/ta.png',
completed: true,
canExecute: false,
progress: { current: 1, total: 1 },
reward: '公会贡献点 x100',
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
settings: { autoExecute: true, delay: 0, notification: true },
details: [
{ id: 1, name: '捐献金币', completed: true }
],
logs: [
{ id: 1, timestamp: Date.now() - 2 * 60 * 60 * 1000, type: 'success', message: '已完成公会贡献' }
]
}
]
}
const onRoleChange = (roleId) => {
selectedRoleId.value = roleId
gameRolesStore.selectRole(
gameRolesStore.gameRoles.find(role => role.id === roleId)
)
if (roleId) {
refreshTasks()
}
}
const onFilterChange = (filter) => {
currentFilter.value = filter
}
const onSearch = (keyword) => {
searchKeyword.value = keyword
}
const executeTask = async (taskId) => {
if (!selectedRoleId.value) {
message.error('请先选择游戏角色')
return
}
try {
// 检查WebSocket连接状态
const wsStatus = localTokenStore.getWebSocketStatus(selectedRoleId.value)
if (wsStatus !== 'connected') {
// 尝试建立连接
const tokenData = localTokenStore.getGameToken(selectedRoleId.value)
if (tokenData) {
localTokenStore.createWebSocketConnection(
selectedRoleId.value,
tokenData.token,
tokenData.wsUrl
)
// 等待一秒让连接建立
await new Promise(resolve => setTimeout(resolve, 1000))
} else {
throw new Error('未找到游戏token请重新添加角色')
}
}
// 模拟通过WebSocket执行任务
console.log(`通过WebSocket执行任务: ${taskId}`)
// 更新本地任务状态
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
if (taskIndex !== -1) {
tasks.value[taskIndex] = {
...tasks.value[taskIndex],
completed: true,
completedAt: new Date().toISOString()
}
// 添加执行日志
if (!tasks.value[taskIndex].logs) {
tasks.value[taskIndex].logs = []
}
tasks.value[taskIndex].logs.push({
id: Date.now(),
timestamp: Date.now(),
type: 'success',
message: `任务 "${tasks.value[taskIndex].title}" 执行成功`
})
// 保存到本地存储
localStorage.setItem(`dailyTasks_${selectedRoleId.value}`, JSON.stringify(tasks.value))
}
message.success('任务执行成功')
} catch (error) {
console.error('执行任务失败:', error)
// 添加错误日志
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
if (taskIndex !== -1) {
if (!tasks.value[taskIndex].logs) {
tasks.value[taskIndex].logs = []
}
tasks.value[taskIndex].logs.push({
id: Date.now(),
timestamp: Date.now(),
type: 'error',
message: `任务执行失败: ${error.message}`
})
}
throw error
}
}
const toggleTaskStatus = (taskId) => {
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
if (taskIndex !== -1) {
tasks.value[taskIndex].completed = !tasks.value[taskIndex].completed
message.info('任务状态已更新')
}
}
const updateTask = (updatedTask) => {
const taskIndex = tasks.value.findIndex(task => task.id === updatedTask.id)
if (taskIndex !== -1) {
tasks.value[taskIndex] = updatedTask
}
}
const handleBulkAction = (key) => {
switch (key) {
case 'execute-all-pending':
executeAllPendingTasks()
break
case 'mark-all-completed':
markAllCompleted()
break
case 'reset-all-tasks':
resetAllTasks()
break
}
}
const executeAllPendingTasks = async () => {
const pendingTasks = tasks.value.filter(task => !task.completed && task.canExecute)
if (pendingTasks.length === 0) {
message.info('没有可执行的待完成任务')
return
}
dialog.confirm({
title: '批量执行任务',
content: `确定要执行 ${pendingTasks.length} 个待完成任务吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
let successCount = 0
let failCount = 0
for (const task of pendingTasks) {
try {
await executeTask(task.id)
successCount++
} catch (error) {
failCount++
}
}
message.info(`批量执行完成:成功 ${successCount} 个,失败 ${failCount}`)
}
})
}
const markAllCompleted = () => {
const pendingTasks = tasks.value.filter(task => !task.completed)
if (pendingTasks.length === 0) {
message.info('所有任务都已完成')
return
}
dialog.confirm({
title: '标记所有任务为已完成',
content: `确定要将 ${pendingTasks.length} 个待完成任务标记为已完成吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
pendingTasks.forEach(task => {
task.completed = true
task.completedAt = new Date().toISOString()
})
message.success('所有任务已标记为完成')
}
})
}
const resetAllTasks = () => {
dialog.confirm({
title: '重置所有任务状态',
content: '确定要重置所有任务状态吗?此操作将清除所有完成记录。',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
tasks.value.forEach(task => {
task.completed = false
task.completedAt = null
})
message.success('所有任务状态已重置')
}
})
}
// 生命周期
onMounted(async () => {
// 确保用户已登录
if (!authStore.isAuthenticated) {
router.push('/login')
return
}
// 初始化游戏角色数据
if (gameRolesStore.gameRoles.length === 0) {
await gameRolesStore.fetchGameRoles()
}
// 页面进入时手动调用阵容加载接口确保WebSocket连接后再调用
if (tokenStore.selectedToken) {
await loadTeamDataWithConnection(tokenStore.selectedToken.id)
}
// 设置默认选中的角色
if (gameRolesStore.selectedRole) {
selectedRoleId.value = gameRolesStore.selectedRole.id
// 尝试从本地存储加载任务数据
const savedTasks = localStorage.getItem(`dailyTasks_${selectedRoleId.value}`)
if (savedTasks) {
try {
tasks.value = JSON.parse(savedTasks)
} catch (error) {
console.error('解析任务数据失败:', error)
refreshTasks()
}
} else {
refreshTasks()
}
} else if (gameRolesStore.gameRoles.length > 0) {
selectedRoleId.value = gameRolesStore.gameRoles[0].id
onRoleChange(selectedRoleId.value)
}
})
// 监听选中角色变化
watch(() => gameRolesStore.selectedRole, (newRole) => {
if (newRole && newRole.id !== selectedRoleId.value) {
selectedRoleId.value = newRole.id
}
})
</script>
<style scoped lang="scss">
.daily-tasks-page {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: var(--spacing-xl) 0;
color: white;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: var(--spacing-lg);
}
.header-left {
flex: 1;
}
.page-title {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-sm);
}
.page-subtitle {
font-size: var(--font-size-lg);
opacity: 0.9;
margin: 0;
}
.header-actions {
display: flex;
gap: var(--spacing-md);
}
.role-selector-section {
background: white;
padding: var(--spacing-lg) 0;
border-bottom: 1px solid var(--border-light);
}
.role-selector {
display: flex;
align-items: center;
gap: var(--spacing-md);
flex-wrap: wrap;
}
.selector-label {
font-weight: var(--font-weight-medium);
color: var(--text-primary);
white-space: nowrap;
}
.role-stats {
display: flex;
gap: var(--spacing-lg);
margin-left: auto;
}
.stat-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.stat-value {
color: var(--text-primary);
font-weight: var(--font-weight-semibold);
}
.filter-section {
background: white;
padding: var(--spacing-md) 0;
border-bottom: 1px solid var(--border-light);
}
.filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-lg);
}
.search-box {
width: 280px;
}
.tasks-section {
padding: var(--spacing-xl) 0;
}
.tasks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: var(--spacing-lg);
}
.empty-state,
.loading-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
// 响应式设计
@media (max-width: 1200px) {
.tasks-grid {
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
}
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-md);
}
.header-actions {
width: 100%;
justify-content: flex-start;
}
.filter-bar {
flex-direction: column;
gap: var(--spacing-md);
}
.role-selector {
flex-direction: column;
align-items: flex-start;
}
.role-stats {
margin-left: 0;
width: 100%;
justify-content: space-between;
}
.tasks-grid {
grid-template-columns: 1fr;
gap: var(--spacing-md);
}
.search-box {
width: 100%;
}
}
</style>