Initial public release

This commit is contained in:
steve
2025-08-22 17:30:53 +08:00
commit 498849dc1e
64 changed files with 20416 additions and 0 deletions

759
src/views/DailyTasks.vue Normal file
View File

@@ -0,0 +1,759 @@
<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'
}
]
// 方法
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()
}
// 设置默认选中的角色
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>

840
src/views/Dashboard.vue Normal file
View File

@@ -0,0 +1,840 @@
<template>
<div class="dashboard-page">
<!-- 顶部导航 -->
<nav class="dashboard-nav">
<div class="nav-container">
<div class="nav-brand">
<img
src="/icons/logo.png"
alt="XYZW"
class="brand-logo"
>
<span class="brand-text">XYZW 控制台</span>
</div>
<div class="nav-menu">
<router-link
to="/dashboard"
class="nav-item"
active-class="active"
>
<n-icon><Home /></n-icon>
<span>首页</span>
</router-link>
<router-link
to="/game-features"
class="nav-item"
active-class="active"
>
<n-icon><Cube /></n-icon>
<span>游戏功能</span>
</router-link>
<router-link
to="/tokens"
class="nav-item"
active-class="active"
>
<n-icon><PersonCircle /></n-icon>
<span>Token管理</span>
</router-link>
<router-link
to="/daily-tasks"
class="nav-item"
active-class="active"
>
<n-icon><Settings /></n-icon>
<span>任务管理</span>
</router-link>
<router-link
to="/message-test"
class="nav-item"
active-class="active"
>
<n-icon><ChatBubbles /></n-icon>
<span>消息测试</span>
</router-link>
<router-link
to="/profile"
class="nav-item"
active-class="active"
>
<n-icon><Settings /></n-icon>
<span>个人设置</span>
</router-link>
</div>
<div class="nav-user">
<n-dropdown
:options="userMenuOptions"
@select="handleUserAction"
>
<div class="user-info">
<n-avatar
size="medium"
fallback-src="/icons/xiaoyugan.png"
/>
<span class="username">{{ tokenStore.selectedToken?.name || '未选择Token' }}</span>
<n-icon><ChevronDown /></n-icon>
</div>
</n-dropdown>
</div>
</div>
</nav>
<!-- 主要内容 -->
<main class="dashboard-main">
<div class="main-container">
<!-- 欢迎区域 -->
<section class="welcome-section">
<div class="welcome-content">
<div class="welcome-text">
<h1>欢迎回来{{ tokenStore.selectedToken?.name || '游戏玩家' }}</h1>
<p>今天是 {{ currentDate }}继续您的游戏管理之旅吧</p>
</div>
<div class="welcome-actions">
<n-button
type="primary"
size="large"
@click="router.push('/game-features')"
>
进入游戏功能
</n-button>
<n-button
size="large"
@click="router.push('/tokens')"
>
管理Token
</n-button>
</div>
</div>
</section>
<!-- 统计卡片 -->
<section class="stats-section">
<div class="stats-grid">
<div
v-for="stat in statistics"
:key="stat.id"
class="stat-card"
>
<div
class="stat-icon"
:style="{ color: stat.color }"
>
<component :is="stat.icon" />
</div>
<div class="stat-content">
<div class="stat-number">
{{ stat.value }}
</div>
<div class="stat-label">
{{ stat.label }}
</div>
<div
class="stat-change"
:class="stat.changeType"
>
{{ stat.change }}
</div>
</div>
</div>
</div>
</section>
<!-- 快速操作 -->
<section class="quick-actions-section">
<h2 class="section-title">
快速操作
</h2>
<div class="actions-grid">
<div
v-for="action in quickActions"
:key="action.id"
class="action-card"
@click="handleQuickAction(action)"
>
<div class="action-icon">
<component :is="action.icon" />
</div>
<div class="action-content">
<h3>{{ action.title }}</h3>
<p>{{ action.description }}</p>
</div>
</div>
</div>
</section>
<!-- 最近活动 -->
<section class="recent-activity-section">
<div class="activity-header">
<h2 class="section-title">
最近活动
</h2>
<n-button
text
type="primary"
@click="refreshActivity"
>
刷新
</n-button>
</div>
<div
v-if="recentActivities.length"
class="activity-list"
>
<div
v-for="activity in recentActivities"
:key="activity.id"
class="activity-item"
>
<div
class="activity-icon"
:class="activity.type"
>
<component :is="getActivityIcon(activity.type)" />
</div>
<div class="activity-content">
<div class="activity-text">
{{ activity.message }}
</div>
<div class="activity-time">
{{ formatTime(activity.timestamp) }}
</div>
</div>
</div>
</div>
<div
v-else
class="empty-activity"
>
<n-empty description="暂无活动记录" />
</div>
</section>
</div>
</main>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { useTokenStore } from '@/stores/tokenStore'
import {
Home,
PersonCircle,
Cube,
Settings,
ChevronDown,
Ribbon,
CheckmarkCircle,
Time,
TrendingUp,
Add,
Chatbubbles,
Cloud
} from '@vicons/ionicons5'
const router = useRouter()
const message = useMessage()
const tokenStore = useTokenStore()
// 响应式数据
const recentActivities = ref([])
// 计算属性
const currentDate = computed(() => {
return new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
})
})
const statistics = computed(() => [
{
id: 1,
icon: PersonCircle,
label: '游戏Token',
value: tokenStore.gameTokens.length,
change: '+2 本月',
changeType: 'positive',
color: '#18a058'
},
{
id: 2,
icon: CheckmarkCircle,
label: '已完成任务',
value: '156',
change: '+12 今日',
changeType: 'positive',
color: '#2080f0'
},
{
id: 3,
icon: Time,
label: '节省时间',
value: '24.5h',
change: '+3.2h 本周',
changeType: 'positive',
color: '#f0a020'
},
{
id: 4,
icon: TrendingUp,
label: '效率提升',
value: '85%',
change: '+15% 本月',
changeType: 'positive',
color: '#d03050'
}
])
const quickActions = ref([
{
id: 1,
icon: Cube,
title: '游戏功能',
description: '访问所有游戏功能模块',
action: 'game-features'
},
{
id: 2,
icon: Add,
title: '添加Token',
description: '快速添加新的游戏Token',
action: 'add-token'
},
{
id: 3,
icon: CheckmarkCircle,
title: '执行任务',
description: '一键执行所有待完成任务',
action: 'execute-tasks'
},
{
id: 4,
icon: Cloud,
title: 'WebSocket测试',
description: '测试WebSocket连接和游戏命令',
action: 'websocket-test'
},
{
id: 5,
icon: Settings,
title: '系统设置',
description: '配置个人偏好和系统选项',
action: 'open-settings'
}
])
const userMenuOptions = [
{
label: '个人资料',
key: 'profile'
},
{
label: '账户设置',
key: 'settings'
},
{
type: 'divider'
},
{
label: '退出登录',
key: 'logout'
}
]
// 方法
const handleUserAction = (key) => {
switch (key) {
case 'profile':
router.push('/profile')
break
case 'settings':
router.push('/settings')
break
case 'logout':
tokenStore.clearAllTokens()
message.success('已清除所有Token')
router.push('/tokens')
break
}
}
const handleQuickAction = (action) => {
switch (action.action) {
case 'game-features':
router.push('/game-features')
break
case 'add-token':
router.push('/tokens')
break
case 'execute-tasks':
router.push('/game-features')
break
case 'websocket-test':
router.push('/websocket-test')
break
case 'open-settings':
router.push('/profile')
break
}
}
const refreshActivity = () => {
// 模拟刷新活动数据
recentActivities.value = [
{
id: 1,
type: 'success',
message: '成功完成日常任务:每日签到',
timestamp: Date.now() - 30 * 60 * 1000
},
{
id: 2,
type: 'info',
message: '添加了新的游戏角色:剑士小明',
timestamp: Date.now() - 2 * 60 * 60 * 1000
},
{
id: 3,
type: 'warning',
message: '任务执行遇到错误,请检查网络连接',
timestamp: Date.now() - 4 * 60 * 60 * 1000
}
]
message.success('活动数据已刷新')
}
const getActivityIcon = (type) => {
switch (type) {
case 'success':
return CheckmarkCircle
case 'warning':
return Time
case 'info':
default:
return Cube
}
}
const formatTime = (timestamp) => {
const diff = Date.now() - timestamp
const minutes = Math.floor(diff / (1000 * 60))
const hours = Math.floor(diff / (1000 * 60 * 60))
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days > 0) {
return `${days}天前`
} else if (hours > 0) {
return `${hours}小时前`
} else if (minutes > 0) {
return `${minutes}分钟前`
} else {
return '刚刚'
}
}
// 生命周期
onMounted(async () => {
// 确保有Token
if (!tokenStore.hasTokens) {
router.push('/tokens')
return
}
// 初始化Token数据
tokenStore.initTokenStore()
refreshActivity()
})
</script>
<style scoped lang="scss">
.dashboard-page {
min-height: 100vh;
background: var(--bg-secondary);
}
// 导航栏
.dashboard-nav {
background: white;
border-bottom: 1px solid var(--border-light);
padding: 0 var(--spacing-lg);
position: sticky;
top: 0;
z-index: var(--z-sticky);
}
.nav-container {
display: flex;
align-items: center;
height: 64px;
max-width: 1400px;
margin: 0 auto;
}
.nav-brand {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-right: var(--spacing-xl);
}
.brand-logo {
width: 32px;
height: 32px;
border-radius: var(--border-radius-small);
}
.brand-text {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.nav-menu {
display: flex;
gap: var(--spacing-md);
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius-medium);
color: var(--text-secondary);
text-decoration: none;
transition: all var(--transition-fast);
&:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
&.active {
background: var(--primary-color-light);
color: var(--primary-color);
}
}
.nav-user {
margin-left: auto;
}
.user-info {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
border-radius: var(--border-radius-medium);
cursor: pointer;
transition: background var(--transition-fast);
&:hover {
background: var(--bg-tertiary);
}
}
.username {
font-weight: var(--font-weight-medium);
color: var(--text-primary);
}
// 主要内容
.dashboard-main {
padding: var(--spacing-xl);
}
.main-container {
max-width: 1400px;
margin: 0 auto;
}
// 欢迎区域
.welcome-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: var(--border-radius-xl);
padding: var(--spacing-2xl);
margin-bottom: var(--spacing-xl);
color: white;
}
.welcome-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-lg);
}
.welcome-text {
h1 {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-sm);
}
p {
font-size: var(--font-size-lg);
opacity: 0.9;
margin: 0;
}
}
.welcome-actions {
display: flex;
gap: var(--spacing-md);
}
// 统计区域
.stats-section {
margin-bottom: var(--spacing-xl);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-lg);
}
.stat-card {
background: white;
border-radius: var(--border-radius-large);
padding: var(--spacing-lg);
box-shadow: var(--shadow-light);
transition: all var(--transition-normal);
&:hover {
box-shadow: var(--shadow-medium);
transform: translateY(-2px);
}
}
.stat-icon {
width: 48px;
height: 48px;
margin-bottom: var(--spacing-md);
:deep(svg) {
width: 100%;
height: 100%;
}
}
.stat-number {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-xs);
}
.stat-change {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
&.positive {
color: var(--success-color);
}
&.negative {
color: var(--error-color);
}
}
// 快速操作区域
.quick-actions-section {
margin-bottom: var(--spacing-xl);
}
.section-title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-lg);
}
.action-card {
background: white;
border-radius: var(--border-radius-large);
padding: var(--spacing-lg);
box-shadow: var(--shadow-light);
cursor: pointer;
transition: all var(--transition-normal);
&:hover {
box-shadow: var(--shadow-medium);
transform: translateY(-2px);
}
}
.action-icon {
width: 40px;
height: 40px;
color: var(--primary-color);
margin-bottom: var(--spacing-md);
:deep(svg) {
width: 100%;
height: 100%;
}
}
.action-content {
h3 {
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin: 0;
}
}
// 最近活动区域
.recent-activity-section {
background: white;
border-radius: var(--border-radius-large);
padding: var(--spacing-xl);
box-shadow: var(--shadow-light);
}
.activity-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.activity-list {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.activity-item {
display: flex;
gap: var(--spacing-md);
padding: var(--spacing-md);
border-radius: var(--border-radius-medium);
transition: background var(--transition-fast);
&:hover {
background: var(--bg-tertiary);
}
}
.activity-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.success {
background: rgba(24, 160, 88, 0.1);
color: var(--success-color);
}
&.warning {
background: rgba(240, 160, 32, 0.1);
color: var(--warning-color);
}
&.info {
background: rgba(32, 128, 240, 0.1);
color: var(--info-color);
}
:deep(svg) {
width: 16px;
height: 16px;
}
}
.activity-content {
flex: 1;
}
.activity-text {
color: var(--text-primary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-xs);
}
.activity-time {
color: var(--text-tertiary);
font-size: var(--font-size-xs);
}
.empty-activity {
text-align: center;
padding: var(--spacing-xl) 0;
}
// 响应式设计
@media (max-width: 1024px) {
.welcome-content {
flex-direction: column;
text-align: center;
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
}
@media (max-width: 768px) {
.dashboard-main {
padding: var(--spacing-md);
}
.nav-menu {
display: none;
}
.welcome-section {
padding: var(--spacing-xl);
}
.welcome-text h1 {
font-size: var(--font-size-2xl);
}
.welcome-actions {
flex-direction: column;
width: 100%;
}
.stats-grid {
grid-template-columns: 1fr;
}
.actions-grid {
grid-template-columns: 1fr;
}
}
</style>

570
src/views/GameFeatures.vue Normal file
View File

@@ -0,0 +1,570 @@
<template>
<div class="game-features-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">
{{ tokenStore.selectedToken?.name || '未选择Token' }}
</p>
</div>
<div class="header-actions">
<div
class="connection-status"
:class="connectionStatus"
>
<n-icon><CloudDone /></n-icon>
<span>{{ connectionStatusText }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 反馈提示区域 -->
<div
v-if="showFeedback"
class="feedback-section"
>
</div>
<!-- 功能模块网格 -->
<div class="features-grid-section">
<div class="container">
<GameStatus />
</div>
</div>
<!-- WebSocket 连接状态 -->
<div class="ws-status-section">
<div class="container">
<div class="ws-status-card">
<div class="status-header">
<h3>连接状态</h3>
<n-button
text
@click="toggleConnection"
>
{{ isConnected ? '断开连接' : '重新连接' }}
</n-button>
</div>
<div class="status-content">
<div class="status-item">
<span>WebSocket状态:</span>
<span :class="connectionClass">{{ connectionStatusText }}</span>
</div>
<div
v-if="tokenStore.selectedToken"
class="status-item"
>
<span>当前Token:</span>
<span>{{ tokenStore.selectedToken.name }}</span>
</div>
<div
v-if="lastActivity"
class="status-item"
>
<span>最后活动:</span>
<span>{{ lastActivity }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { useTokenStore } from '@/stores/tokenStore'
import GameStatus from '@/components/GameStatus.vue'
import { CloudDone } from '@vicons/ionicons5'
const router = useRouter()
const message = useMessage()
const tokenStore = useTokenStore()
// 响应式数据
const showFeedback = ref(true)
const lastActivity = ref(null)
// 计算属性
const connectionStatus = computed(() => {
if (!tokenStore.selectedToken) return 'disconnected'
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
return status === 'connected' ? 'connected' : 'disconnected'
})
const connectionStatusText = computed(() => {
if (!tokenStore.selectedToken) return '未选择Token'
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
return status === 'connected' ? '已连接' : '未连接'
})
const connectionClass = computed(() => {
return connectionStatus.value === 'connected' ? 'status-connected' : 'status-disconnected'
})
// 方法
const handleFeatureAction = (featureType) => {
if (!tokenStore.selectedToken) {
message.warning('请先选择Token')
router.push('/tokens')
return
}
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
if (status !== 'connected') {
message.warning('WebSocket未连接请先建立连接')
return
}
const tokenId = tokenStore.selectedToken.id
const actions = {
'team-challenge': () => {
message.info('开始执行队伍挑战...')
tokenStore.sendMessage(tokenId, 'fight_startareaarena')
},
'daily-tasks': () => {
message.info('启动每日任务服务...')
tokenStore.sendMessage(tokenId, 'task_claimdailyreward')
},
'salt-robot': () => {
message.info('领取盐罐机器人奖励...')
tokenStore.sendMessage(tokenId, 'bottlehelper_claim')
},
'idle-time': () => {
message.info('领取挂机时间奖励...')
tokenStore.sendMessage(tokenId, 'system_claimhangupreward')
},
'power-switch': () => {
message.info('执行威震大开关...')
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
},
'club-ranking': () => {
message.info('报名俱乐部排位...')
tokenStore.sendMessage(tokenId, 'legionmatch_rolesignup')
},
'club-checkin': () => {
message.info('执行俱乐部签到...')
tokenStore.sendMessage(tokenId, 'legion_signin')
},
'tower-challenge': () => {
message.info('开始爬塔挑战...')
tokenStore.sendMessage(tokenId, 'fight_starttower')
}
}
const action = actions[featureType]
if (action) {
action()
} else {
message.warning('功能暂未实现')
}
}
// 已移除 sendWebSocketMessage使用 tokenStore.sendMessage 代替
const connectWebSocket = () => {
if (!tokenStore.selectedToken) {
message.warning('请先选择一个Token')
router.push('/tokens')
return
}
try {
const tokenId = tokenStore.selectedToken.id
const token = tokenStore.selectedToken.token
// 使用 tokenStore 的 WebSocket 连接管理
tokenStore.createWebSocketConnection(tokenId, token)
message.info('正在建立 WebSocket 连接...')
// 等待连接建立
setTimeout(async () => {
const status = tokenStore.getWebSocketStatus(tokenId)
if (status === 'connected') {
message.success('WebSocket 连接成功')
// 连接成功后自动初始化游戏数据
await initializeGameData()
}
}, 2000)
} catch (error) {
console.error('WebSocket连接失败:', error)
message.error('WebSocket连接失败')
}
}
const disconnectWebSocket = () => {
if (tokenStore.selectedToken) {
const tokenId = tokenStore.selectedToken.id
tokenStore.closeWebSocketConnection(tokenId)
message.info('WebSocket连接已断开')
}
}
const toggleConnection = () => {
if (connectionStatus.value === 'connected') {
disconnectWebSocket()
} else {
connectWebSocket()
}
}
// handleWebSocketMessage 已移除,消息处理由 tokenStore 负责
// 生命周期
onMounted(() => {
// 检查是否需要连接 WebSocket
if (tokenStore.selectedToken) {
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
if (status !== 'connected') {
connectWebSocket()
} else {
// 如果已连接,立即获取初始数据
initializeGameData()
}
}
})
// 初始化游戏数据
const initializeGameData = async () => {
if (!tokenStore.selectedToken) return
try {
const tokenId = tokenStore.selectedToken.id
console.log('🎮 初始化游戏数据...')
// 获取角色信息
console.log('🎮 正在获取角色信息...')
const roleResult = tokenStore.sendMessage(tokenId, 'role_getroleinfo')
console.log('🎮 角色信息请求结果:', roleResult)
// 获取塔信息
console.log('🎮 正在获取塔信息...')
const towerResult = tokenStore.sendMessage(tokenId, 'tower_getinfo')
console.log('🎮 塔信息请求结果:', towerResult)
// 获取队伍信息
console.log('🎮 正在获取队伍信息...')
const teamResult = tokenStore.sendMessage(tokenId, 'presetteam_getteam')
console.log('🎮 队伍信息请求结果:', teamResult)
console.log('🎮 游戏数据初始化请求已发送')
} catch (error) {
console.warn('初始化游戏数据失败:', error)
}
}
onUnmounted(() => {
// WebSocket 连接由 tokenStore 管理,不需要手动清理
})
</script>
<style scoped lang="scss">
.game-features-page {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
// 页面头部
.page-header {
background: white;
border-bottom: 1px solid var(--border-light);
padding: var(--spacing-lg) 0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 var(--spacing-lg);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin: 0 0 var(--spacing-xs) 0;
}
.page-subtitle {
color: var(--text-secondary);
font-size: var(--font-size-md);
margin: 0;
}
.connection-status {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius-medium);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
&.connected {
background: rgba(24, 160, 88, 0.1);
color: var(--success-color);
}
&.disconnected {
background: rgba(208, 48, 80, 0.1);
color: var(--error-color);
}
}
// 反馈提示区域
.feedback-section {
padding: var(--spacing-md) 0;
}
// 功能模块网格
.features-grid-section {
padding: var(--spacing-xl) 0;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: var(--spacing-lg);
}
.feature-card {
background: white;
border-radius: var(--border-radius-xl);
padding: var(--spacing-lg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all var(--transition-normal);
border-left: 4px solid var(--primary-color);
&:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
// 不同功能的主题色
&.team-challenge { border-left-color: #2080f0; }
&.daily-tasks { border-left-color: #f0a020; }
&.salt-robot { border-left-color: #18a058; }
&.idle-time { border-left-color: #d03050; }
&.power-switch { border-left-color: #7c3aed; }
&.club-ranking { border-left-color: #f59e0b; }
&.club-checkin { border-left-color: #10b981; }
&.tower-challenge { border-left-color: #6366f1; }
}
.card-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.feature-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius-medium);
background: var(--primary-color-light);
color: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
:deep(svg) {
width: 24px;
height: 24px;
}
}
.feature-title {
flex: 1;
h3 {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin: 0 0 var(--spacing-xs) 0;
}
}
.feature-subtitle {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.feature-badge, .feature-status {
flex-shrink: 0;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-small);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
}
.feature-status {
&.in-progress {
background: rgba(240, 160, 32, 0.1);
color: var(--warning-color);
}
&.completed {
background: rgba(24, 160, 88, 0.1);
color: var(--success-color);
}
&.waiting {
background: rgba(32, 128, 240, 0.1);
color: var(--info-color);
}
}
.card-content {
margin-bottom: var(--spacing-lg);
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
.stage-text {
font-weight: var(--font-weight-medium);
color: var(--text-primary);
}
.progress-text {
font-weight: var(--font-weight-medium);
color: var(--text-secondary);
}
}
.time-display {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
text-align: center;
margin-bottom: var(--spacing-sm);
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
}
.task-description {
color: var(--text-secondary);
font-size: var(--font-size-sm);
line-height: 1.5;
}
.card-actions {
margin-top: var(--spacing-lg);
}
// WebSocket状态区域
.ws-status-section {
padding: 0 0 var(--spacing-xl) 0;
}
.ws-status-card {
background: white;
border-radius: var(--border-radius-large);
padding: var(--spacing-lg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
h3 {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin: 0;
}
}
.status-content {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border-light);
&:last-child {
border-bottom: none;
}
span:first-child {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
span:last-child {
font-weight: var(--font-weight-medium);
font-size: var(--font-size-sm);
}
}
.status-connected {
color: var(--success-color);
}
.status-disconnected {
color: var(--error-color);
}
// 响应式设计
@media (max-width: 1024px) {
.features-grid {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
}
@media (max-width: 768px) {
.container {
padding: 0 var(--spacing-md);
}
.header-content {
flex-direction: column;
gap: var(--spacing-md);
text-align: center;
}
.features-grid {
grid-template-columns: 1fr;
}
.feature-card {
padding: var(--spacing-md);
}
.card-header {
flex-direction: column;
text-align: center;
gap: var(--spacing-sm);
}
}
</style>

575
src/views/GameRoles.vue Normal file
View File

@@ -0,0 +1,575 @@
<template>
<div class="game-roles-page">
<div class="container">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h1>游戏角色</h1>
<p>管理您的所有游戏角色</p>
</div>
<div class="header-actions">
<n-button
type="primary"
size="large"
@click="showAddModal = true"
>
<template #icon>
<n-icon><Add /></n-icon>
</template>
添加角色
</n-button>
</div>
</div>
</div>
<!-- 角色列表 -->
<div
v-if="gameRolesStore.gameRoles.length"
class="roles-grid"
>
<div
v-for="role in gameRolesStore.gameRoles"
:key="role.id"
class="role-card"
:class="{ active: role.id === gameRolesStore.selectedRole?.id }"
@click="selectRole(role)"
>
<div class="card-header">
<div class="role-avatar">
<img
:src="role.avatar || '/icons/xiaoyugan.png'"
:alt="role.name"
>
</div>
<div class="role-actions">
<n-dropdown
:options="roleMenuOptions"
@select="(key) => handleRoleAction(key, role)"
>
<n-button text>
<template #icon>
<n-icon><EllipsisHorizontal /></n-icon>
</template>
</n-button>
</n-dropdown>
</div>
</div>
<div class="card-body">
<h3 class="role-name">
{{ role.name }}
</h3>
<p class="role-info">
{{ role.server }} | {{ role.level }}
</p>
<div class="role-tags">
<n-tag
size="small"
:type="role.isActive ? 'success' : 'default'"
>
{{ role.isActive ? '活跃' : '离线' }}
</n-tag>
<n-tag
v-if="role.vip"
size="small"
>
VIP
</n-tag>
</div>
</div>
<div class="card-footer">
<div class="role-stats">
<div class="stat-item">
<span class="stat-label">经验</span>
<span class="stat-value">{{ role.exp || '0' }}</span>
</div>
<div class="stat-item">
<span class="stat-label">金币</span>
<span class="stat-value">{{ formatNumber(role.gold || 0) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div
v-else
class="empty-state"
>
<n-empty
description="暂无游戏角色"
size="large"
>
<template #icon>
<n-icon size="64">
<PersonCircle />
</n-icon>
</template>
<template #extra>
<n-button
type="primary"
size="large"
@click="showAddModal = true"
>
添加第一个角色
</n-button>
</template>
</n-empty>
</div>
<!-- 添加/编辑角色模态框 -->
<n-modal
v-model:show="showAddModal"
preset="card"
title="添加游戏角色"
style="width: 500px"
>
<n-form
ref="roleFormRef"
:model="roleForm"
:rules="roleRules"
label-placement="left"
label-width="80px"
>
<n-form-item
label="角色名称"
path="name"
>
<n-input
v-model:value="roleForm.name"
placeholder="请输入角色名称"
/>
</n-form-item>
<n-form-item
label="服务器"
path="server"
>
<n-select
v-model:value="roleForm.server"
:options="serverOptions"
placeholder="请选择服务器"
/>
</n-form-item>
<n-form-item
label="职业"
path="profession"
>
<n-select
v-model:value="roleForm.profession"
:options="professionOptions"
placeholder="请选择职业"
/>
</n-form-item>
<n-form-item
label="等级"
path="level"
>
<n-input-number
v-model:value="roleForm.level"
:min="1"
:max="200"
placeholder="角色等级"
/>
</n-form-item>
<n-form-item label="账号信息">
<n-input
v-model:value="roleForm.account"
placeholder="游戏账号(可选)"
/>
</n-form-item>
<n-form-item label="备注">
<n-input
v-model:value="roleForm.note"
type="textarea"
placeholder="角色备注信息(可选)"
:rows="3"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="modal-actions">
<n-button @click="showAddModal = false">
取消
</n-button>
<n-button
type="primary"
:loading="isSubmitting"
@click="handleSubmit"
>
{{ editingRole ? '保存' : '添加' }}
</n-button>
</div>
</template>
</n-modal>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage, useDialog } from 'naive-ui'
import { useGameRolesStore } from '@/stores/gameRoles'
import { PersonCircle, Add, EllipsisHorizontal } from '@vicons/ionicons5'
const router = useRouter()
const message = useMessage()
const dialog = useDialog()
const gameRolesStore = useGameRolesStore()
// 响应式数据
const showAddModal = ref(false)
const isSubmitting = ref(false)
const editingRole = ref(null)
const roleFormRef = ref(null)
// 角色表单
const roleForm = reactive({
name: '',
server: '',
profession: '',
level: 1,
account: '',
note: ''
})
// 表单验证规则
const roleRules = {
name: [
{ required: true, message: '请输入角色名称', trigger: 'blur' }
],
server: [
{ required: true, message: '请选择服务器', trigger: 'change' }
],
profession: [
{ required: true, message: '请选择职业', trigger: 'change' }
],
level: [
{ required: true, type: 'number', message: '请输入角色等级', trigger: 'blur' }
]
}
// 选项数据
const serverOptions = [
{ label: '风云服', value: '风云服' },
{ label: '神话服', value: '神话服' },
{ label: '传奇服', value: '传奇服' },
{ label: '梦幻服', value: '梦幻服' },
{ label: '英雄服', value: '英雄服' }
]
const professionOptions = [
{ label: '战士', value: '战士' },
{ label: '法师', value: '法师' },
{ label: '道士', value: '道士' },
{ label: '刺客', value: '刺客' },
{ label: '弓手', value: '弓手' },
{ label: '牧师', value: '牧师' }
]
const roleMenuOptions = [
{ label: '编辑', key: 'edit' },
{ label: '设为主角色', key: 'set-primary' },
{ label: '查看详情', key: 'view-details' },
{ type: 'divider' },
{ label: '删除', key: 'delete' }
]
// 方法
const selectRole = (role) => {
gameRolesStore.selectRole(role)
message.success(`已切换到角色:${role.name}`)
}
const handleRoleAction = async (key, role) => {
switch (key) {
case 'edit':
editRole(role)
break
case 'set-primary':
selectRole(role)
break
case 'view-details':
viewRoleDetails(role)
break
case 'delete':
deleteRole(role)
break
}
}
const editRole = (role) => {
editingRole.value = role
Object.assign(roleForm, role)
showAddModal.value = true
}
const viewRoleDetails = (role) => {
message.info('角色详情功能开发中...')
}
const deleteRole = (role) => {
dialog.warning({
title: '删除角色',
content: `确定要删除角色 "${role.name}" 吗?此操作无法恢复。`,
positiveText: '确定删除',
negativeText: '取消',
onPositiveClick: async () => {
const result = await gameRolesStore.deleteGameRole(role.id)
if (result.success) {
message.success(result.message)
} else {
message.error(result.message)
}
}
})
}
const handleSubmit = async () => {
if (!roleFormRef.value) return
try {
await roleFormRef.value.validate()
isSubmitting.value = true
let result
if (editingRole.value) {
// 编辑模式
result = await gameRolesStore.updateGameRole(editingRole.value.id, roleForm)
} else {
// 添加模式
result = await gameRolesStore.addGameRole(roleForm)
}
if (result.success) {
message.success(result.message)
showAddModal.value = false
resetForm()
} else {
message.error(result.message)
}
} catch (error) {
// 表单验证失败
} finally {
isSubmitting.value = false
}
}
const resetForm = () => {
Object.keys(roleForm).forEach(key => {
roleForm[key] = key === 'level' ? 1 : ''
})
editingRole.value = null
}
const formatNumber = (num) => {
if (num >= 100000000) {
return (num / 100000000).toFixed(1) + '亿'
} else if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
}
return num.toString()
}
// 生命周期
onMounted(async () => {
// 获取游戏角色列表
if (gameRolesStore.gameRoles.length === 0) {
await gameRolesStore.fetchGameRoles()
}
})
</script>
<style scoped lang="scss">
.game-roles-page {
min-height: 100vh;
background: var(--bg-secondary);
padding: var(--spacing-xl) 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-lg);
}
.page-header {
margin-bottom: var(--spacing-2xl);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-lg);
}
.header-left {
h1 {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
p {
color: var(--text-secondary);
font-size: var(--font-size-lg);
margin: 0;
}
}
.roles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--spacing-lg);
}
.role-card {
background: white;
border-radius: var(--border-radius-large);
box-shadow: var(--shadow-light);
overflow: hidden;
cursor: pointer;
transition: all var(--transition-normal);
border: 2px solid transparent;
&:hover {
box-shadow: var(--shadow-medium);
transform: translateY(-2px);
}
&.active {
border-color: var(--primary-color);
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-lg) var(--spacing-lg) 0;
}
.role-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
overflow: hidden;
border: 3px solid var(--border-light);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.role-actions {
opacity: 0;
transition: opacity var(--transition-fast);
.role-card:hover & {
opacity: 1;
}
}
.card-body {
padding: var(--spacing-md) var(--spacing-lg);
text-align: center;
}
.role-name {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.role-info {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-md);
}
.role-tags {
display: flex;
gap: var(--spacing-xs);
justify-content: center;
}
.card-footer {
padding: var(--spacing-md) var(--spacing-lg) var(--spacing-lg);
border-top: 1px solid var(--border-light);
}
.role-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
.stat-item {
text-align: center;
}
.stat-label {
display: block;
color: var(--text-tertiary);
font-size: var(--font-size-xs);
margin-bottom: var(--spacing-xs);
}
.stat-value {
color: var(--text-primary);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.modal-actions {
display: flex;
gap: var(--spacing-md);
justify-content: flex-end;
}
@media (max-width: 768px) {
.container {
padding: 0 var(--spacing-md);
}
.header-content {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-md);
}
.header-actions {
width: 100%;
}
.roles-grid {
grid-template-columns: 1fr;
}
.role-stats {
grid-template-columns: 1fr;
}
}
</style>

605
src/views/Home.vue Normal file
View File

@@ -0,0 +1,605 @@
<template>
<div class="home-page">
<!-- 导航栏 -->
<nav class="navbar glass">
<div class="container">
<div class="nav-content">
<div class="nav-brand">
<img
src="/icons/logo.png"
alt="XYZW"
class="brand-logo"
>
<span class="brand-text">XYZW 游戏管理系统</span>
</div>
<div class="nav-actions">
<template v-if="!authStore.isAuthenticated">
<n-button
text
type="primary"
size="large"
@click="router.push('/login')"
>
登录
</n-button>
<n-button
type="primary"
size="large"
@click="router.push('/register')"
>
注册
</n-button>
</template>
<template v-else>
<n-button
type="primary"
size="large"
@click="router.push('/dashboard')"
>
进入控制台
</n-button>
</template>
</div>
</div>
</div>
</nav>
<!-- 主要内容 -->
<main class="main-content">
<!-- 英雄区域 -->
<section class="hero-section">
<div class="container">
<div class="hero-content">
<div class="hero-text">
<h1 class="hero-title">
专业的游戏管理平台
</h1>
<p class="hero-subtitle">
让游戏变得更简单让管理变得更高效
</p>
<div class="hero-actions">
<n-button
type="primary"
size="large"
class="hero-button"
@click="router.push(authStore.isAuthenticated ? '/dashboard' : '/register')"
>
{{ authStore.isAuthenticated ? '进入控制台' : '立即开始' }}
</n-button>
<n-button
text
type="primary"
size="large"
class="hero-button"
@click="scrollToFeatures"
>
了解更多
</n-button>
</div>
</div>
<div class="hero-visual">
<div class="feature-cards">
<div
v-for="card in featureCards"
:key="card.id"
class="feature-card"
>
<div class="card-icon">
<component :is="card.icon" />
</div>
<div class="card-content">
<h3>{{ card.title }}</h3>
<p>{{ card.description }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 功能特性 -->
<section
ref="featuresSection"
class="features-section"
>
<div class="container">
<div class="section-header">
<h2 class="section-title">
核心功能
</h2>
<p class="section-subtitle">
为您提供全方位的游戏管理解决方案
</p>
</div>
<div class="features-grid">
<div
v-for="feature in features"
:key="feature.id"
class="feature-item"
>
<div class="feature-icon">
<component :is="feature.icon" />
</div>
<h3 class="feature-title">
{{ feature.title }}
</h3>
<p class="feature-description">
{{ feature.description }}
</p>
</div>
</div>
</div>
</section>
<!-- 统计数据 -->
<section class="stats-section">
<div class="container">
<div class="stats-grid">
<div
v-for="stat in stats"
:key="stat.id"
class="stat-item"
>
<div class="stat-number">
{{ stat.number }}
</div>
<div class="stat-label">
{{ stat.label }}
</div>
</div>
</div>
</div>
</section>
</main>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<div class="footer-content">
<div class="footer-brand">
<img
src="/icons/logo.png"
alt="XYZW"
class="footer-logo"
>
<span class="footer-text">XYZW 游戏管理系统</span>
</div>
<div class="footer-links">
<a
href="#"
class="footer-link"
>关于我们</a>
<a
href="#"
class="footer-link"
>隐私政策</a>
<a
href="#"
class="footer-link"
>服务条款</a>
<a
href="#"
class="footer-link"
>联系我们</a>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2024 XYZW. All rights reserved.</p>
</div>
</div>
</footer>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import {
PersonCircle,
Cube,
Ribbon,
Home,
Settings
} from '@vicons/ionicons5'
const router = useRouter()
const authStore = useAuthStore()
const featuresSection = ref(null)
// 功能卡片数据
const featureCards = ref([
{
id: 1,
icon: PersonCircle,
title: '角色管理',
description: '统一管理游戏角色'
},
{
id: 2,
icon: Cube,
title: '任务系统',
description: '自动化日常任务'
},
{
id: 3,
icon: Ribbon,
title: '数据统计',
description: '全面的数据分析'
}
])
// 功能特性数据
const features = ref([
{
id: 1,
icon: PersonCircle,
title: '角色管理',
description: '轻松管理多个游戏角色,统一查看角色信息、等级进度和装备状态'
},
{
id: 2,
icon: Cube,
title: '任务自动化',
description: '智能日常任务系统,自动完成重复性任务,节省您的宝贵时间'
},
{
id: 3,
icon: Ribbon,
title: '数据分析',
description: '详细的数据统计和分析报告,帮助您更好地了解游戏进度'
},
{
id: 4,
icon: Settings,
title: '个性化设置',
description: '灵活的配置选项,根据您的需求定制最适合的管理方案'
}
])
// 统计数据
const stats = ref([
{ id: 1, number: '1000+', label: '活跃用户' },
{ id: 2, number: '50K+', label: '管理角色' },
{ id: 3, number: '100K+', label: '完成任务' },
{ id: 4, number: '99.9%', label: '系统稳定性' }
])
// 滚动到功能区域
const scrollToFeatures = () => {
if (featuresSection.value) {
featuresSection.value.scrollIntoView({
behavior: 'smooth'
})
}
}
onMounted(() => {
// 初始化认证状态
authStore.initAuth()
})
</script>
<style scoped lang="scss">
.home-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
overflow-x: hidden;
}
// 导航栏
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: var(--z-fixed);
padding: var(--spacing-md) 0;
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-brand {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.brand-logo {
width: 32px;
height: 32px;
border-radius: var(--border-radius-small);
}
.brand-text {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: white;
}
.nav-actions {
display: flex;
gap: var(--spacing-sm);
}
// 主要内容
.main-content {
padding-top: 80px;
}
// 英雄区域
.hero-section {
padding: var(--spacing-2xl) 0;
min-height: 80vh;
display: flex;
align-items: center;
}
.hero-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-2xl);
align-items: center;
}
.hero-text {
color: white;
}
.hero-title {
font-size: 3.5rem;
font-weight: var(--font-weight-bold);
line-height: var(--line-height-tight);
margin-bottom: var(--spacing-md);
background: linear-gradient(45deg, #fff, #e6f7ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: var(--font-size-xl);
opacity: 0.9;
margin-bottom: var(--spacing-xl);
line-height: var(--line-height-relaxed);
}
.hero-actions {
display: flex;
gap: var(--spacing-md);
}
.hero-button {
padding: var(--spacing-md) var(--spacing-xl);
font-size: var(--font-size-lg);
}
// 功能卡片
.feature-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-md);
}
.feature-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: var(--border-radius-large);
padding: var(--spacing-lg);
transition: all var(--transition-normal);
&:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
}
.card-icon {
width: 48px;
height: 48px;
color: #fff;
margin-bottom: var(--spacing-md);
:deep(svg) {
width: 100%;
height: 100%;
}
}
.card-content h3 {
color: white;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-sm);
}
.card-content p {
color: rgba(255, 255, 255, 0.8);
font-size: var(--font-size-sm);
line-height: var(--line-height-relaxed);
}
// 功能特性区域
.features-section {
padding: var(--spacing-2xl) 0;
background: white;
}
.section-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
}
.section-title {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-md);
}
.section-subtitle {
font-size: var(--font-size-lg);
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
line-height: var(--line-height-relaxed);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-xl);
}
.feature-item {
text-align: center;
padding: var(--spacing-xl);
border-radius: var(--border-radius-large);
transition: all var(--transition-normal);
&:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-heavy);
}
}
.feature-icon {
width: 64px;
height: 64px;
margin: 0 auto var(--spacing-lg);
color: var(--primary-color);
:deep(svg) {
width: 100%;
height: 100%;
}
}
.feature-title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin-bottom: var(--spacing-md);
}
.feature-description {
color: var(--text-secondary);
line-height: var(--line-height-relaxed);
}
// 统计区域
.stats-section {
padding: var(--spacing-2xl) 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-xl);
}
.stat-item {
text-align: center;
color: white;
}
.stat-number {
font-size: 3rem;
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-sm);
}
.stat-label {
font-size: var(--font-size-lg);
opacity: 0.9;
}
// 页脚
.footer {
background: rgba(0, 0, 0, 0.8);
color: white;
padding: var(--spacing-xl) 0;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.footer-brand {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.footer-logo {
width: 24px;
height: 24px;
}
.footer-text {
font-weight: var(--font-weight-medium);
}
.footer-links {
display: flex;
gap: var(--spacing-lg);
}
.footer-link {
color: rgba(255, 255, 255, 0.8);
transition: color var(--transition-fast);
&:hover {
color: white;
}
}
.footer-bottom {
text-align: center;
padding-top: var(--spacing-lg);
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
}
// 响应式设计
@media (max-width: 768px) {
.hero-content {
grid-template-columns: 1fr;
text-align: center;
}
.hero-title {
font-size: 2.5rem;
}
.hero-actions {
justify-content: center;
}
.footer-content {
flex-direction: column;
gap: var(--spacing-lg);
}
.nav-actions {
flex-direction: column;
gap: var(--spacing-xs);
}
}
</style>

554
src/views/Login.vue Normal file
View File

@@ -0,0 +1,554 @@
<template>
<div class="login-page">
<div class="login-container">
<!-- 登录表单卡片 -->
<div class="login-card glass">
<div class="card-header">
<div class="brand">
<img
src="/icons/logo.png"
alt="XYZW"
class="brand-logo"
>
<h1 class="brand-title">
XYZW 游戏管理系统
</h1>
</div>
<p class="welcome-text">
欢迎回来请登录您的账户
</p>
</div>
<div class="card-body">
<n-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
size="large"
:show-label="false"
>
<n-form-item path="username">
<n-input
v-model:value="loginForm.username"
placeholder="用户名或邮箱"
:input-props="{ autocomplete: 'username' }"
>
<template #prefix>
<n-icon>
<PersonCircle />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="password">
<n-input
v-model:value="loginForm.password"
type="password"
placeholder="密码"
:input-props="{ autocomplete: 'current-password' }"
@keydown.enter="handleLogin"
>
<template #prefix>
<n-icon>
<Lock />
</n-icon>
</template>
</n-input>
</n-form-item>
<div class="form-options">
<n-checkbox v-model:checked="loginForm.rememberMe">
记住我
</n-checkbox>
<n-button
text
type="primary"
@click="router.push('/forgot-password')"
>
忘记密码
</n-button>
</div>
<n-button
type="primary"
size="large"
block
:loading="authStore.isLoading"
class="login-button"
@click="handleLogin"
>
登录
</n-button>
</n-form>
<n-divider>
<span class="divider-text">其他登录方式</span>
</n-divider>
<div class="social-login">
<n-button
size="large"
class="social-button"
@click="handleSocialLogin('qq')"
>
<template #icon>
<n-icon>
<PersonCircle />
</n-icon>
</template>
QQ登录
</n-button>
<n-button
size="large"
class="social-button"
@click="handleSocialLogin('wechat')"
>
<template #icon>
<n-icon>
<PersonCircle />
</n-icon>
</template>
微信登录
</n-button>
</div>
<div class="register-prompt">
<span>还没有账户</span>
<n-button
text
type="primary"
@click="router.push('/register')"
>
立即注册
</n-button>
</div>
</div>
</div>
<!-- 功能展示 -->
<div class="features-showcase">
<div class="showcase-header">
<h2>为什么选择 XYZW</h2>
<p>专业的游戏管理平台让游戏变得更轻松</p>
</div>
<div class="features-list">
<div
v-for="feature in features"
:key="feature.id"
class="feature-item"
>
<div class="feature-icon">
<component :is="feature.icon" />
</div>
<div class="feature-content">
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- 背景装饰 -->
<div class="background-decoration">
<div class="decoration-circle circle-1" />
<div class="decoration-circle circle-2" />
<div class="decoration-circle circle-3" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { useAuthStore } from '@/stores/auth'
import { PersonCircle, Lock, Cube, Ribbon, Settings } from '@vicons/ionicons5'
const router = useRouter()
const message = useMessage()
const authStore = useAuthStore()
const loginFormRef = ref(null)
// 登录表单数据
const loginForm = reactive({
username: '',
password: '',
rememberMe: false
})
// 表单验证规则
const loginRules = {
username: [
{
required: true,
message: '请输入用户名或邮箱',
trigger: ['input', 'blur']
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['input', 'blur']
},
{
min: 6,
message: '密码长度不能少于6位',
trigger: ['input', 'blur']
}
]
}
// 功能特性数据
const features = [
{
id: 1,
icon: PersonCircle,
title: '多角色管理',
description: '统一管理多个游戏角色,随时切换查看'
},
{
id: 2,
icon: Cube,
title: '任务自动化',
description: '智能执行日常任务,解放双手节省时间'
},
{
id: 3,
icon: Ribbon,
title: '数据统计',
description: '详细的进度统计,让游戏数据一目了然'
},
{
id: 4,
icon: Settings,
title: '个性化配置',
description: '灵活的设置选项,打造专属管理方案'
}
]
// 处理登录
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
const result = await authStore.login({
username: loginForm.username,
password: loginForm.password,
rememberMe: loginForm.rememberMe
})
if (result.success) {
message.success('登录成功')
// 跳转到dashboard或之前访问的页面
const redirect = router.currentRoute.value.query.redirect || '/dashboard'
router.push(redirect)
} else {
message.error(result.message)
}
} catch (error) {
// 表单验证失败
console.error('Login validation failed:', error)
}
}
// 处理社交登录
const handleSocialLogin = (provider) => {
message.info(`${provider === 'qq' ? 'QQ' : '微信'}登录功能开发中...`)
}
onMounted(() => {
// 如果已经登录,直接跳转
if (authStore.isAuthenticated) {
router.push('/dashboard')
}
})
</script>
<style scoped lang="scss">
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.login-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-2xl);
max-width: 1200px;
width: 100%;
padding: var(--spacing-lg);
}
.login-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: var(--border-radius-xl);
padding: var(--spacing-2xl);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-header {
text-align: center;
margin-bottom: var(--spacing-xl);
}
.brand {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.brand-logo {
width: 64px;
height: 64px;
border-radius: var(--border-radius-large);
}
.brand-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin: 0;
}
.welcome-text {
color: var(--text-secondary);
font-size: var(--font-size-md);
margin: 0;
}
.card-body {
.n-form {
.n-form-item {
margin-bottom: var(--spacing-lg);
}
}
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.login-button {
height: 48px;
font-size: var(--font-size-md);
font-weight: var(--font-weight-medium);
margin-bottom: var(--spacing-lg);
}
.divider-text {
color: var(--text-tertiary);
font-size: var(--font-size-sm);
}
.social-login {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.social-button {
height: 44px;
border: 1px solid var(--border-light);
&:hover {
border-color: var(--primary-color);
}
}
.register-prompt {
text-align: center;
color: var(--text-secondary);
span {
margin-right: var(--spacing-sm);
}
}
// 功能展示区域
.features-showcase {
color: white;
padding: var(--spacing-xl);
}
.showcase-header {
text-align: center;
margin-bottom: var(--spacing-xl);
h2 {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-md);
}
p {
font-size: var(--font-size-lg);
opacity: 0.9;
margin: 0;
}
}
.features-list {
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
}
.feature-item {
display: flex;
gap: var(--spacing-lg);
padding: var(--spacing-lg);
background: rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius-large);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all var(--transition-normal);
&:hover {
transform: translateX(8px);
background: rgba(255, 255, 255, 0.15);
}
}
.feature-icon {
width: 48px;
height: 48px;
color: white;
flex-shrink: 0;
:deep(svg) {
width: 100%;
height: 100%;
}
}
.feature-content {
flex: 1;
h3 {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-sm);
}
p {
opacity: 0.8;
line-height: var(--line-height-relaxed);
margin: 0;
}
}
// 背景装饰
.background-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: -1;
}
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.05);
animation: float 6s ease-in-out infinite;
}
.circle-1 {
width: 200px;
height: 200px;
top: 10%;
right: 10%;
animation-delay: 0s;
}
.circle-2 {
width: 150px;
height: 150px;
bottom: 20%;
left: 15%;
animation-delay: 2s;
}
.circle-3 {
width: 100px;
height: 100px;
top: 60%;
right: 20%;
animation-delay: 4s;
}
@keyframes float {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
}
}
// 响应式设计
@media (max-width: 1024px) {
.login-container {
grid-template-columns: 1fr;
max-width: 500px;
}
.features-showcase {
order: -1;
}
.showcase-header h2 {
font-size: var(--font-size-2xl);
}
}
@media (max-width: 640px) {
.login-container {
padding: var(--spacing-md);
}
.login-card {
padding: var(--spacing-xl);
}
.brand-title {
font-size: var(--font-size-xl);
}
.social-login {
grid-template-columns: 1fr;
}
.feature-item {
flex-direction: column;
text-align: center;
}
.decoration-circle {
display: none;
}
}
</style>

116
src/views/NotFound.vue Normal file
View File

@@ -0,0 +1,116 @@
<template>
<div class="not-found-page">
<div class="container">
<div class="error-content">
<div class="error-visual">
<div class="error-number">
404
</div>
<div class="error-icon">
<n-icon size="120">
<Search />
</n-icon>
</div>
</div>
<div class="error-text">
<h1>页面未找到</h1>
<p>抱歉您访问的页面不存在或已被移除</p>
</div>
<div class="error-actions">
<n-button
type="primary"
size="large"
@click="router.push('/')"
>
返回首页
</n-button>
<n-button
size="large"
@click="router.back()"
>
返回上一页
</n-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { Search } from '@vicons/ionicons5'
const router = useRouter()
</script>
<style scoped lang="scss">
.not-found-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-lg);
}
.error-content {
text-align: center;
color: white;
max-width: 600px;
}
.error-visual {
position: relative;
margin-bottom: var(--spacing-xl);
}
.error-number {
font-size: 12rem;
font-weight: var(--font-weight-bold);
opacity: 0.1;
line-height: 1;
}
.error-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0.6;
}
.error-text {
margin-bottom: var(--spacing-2xl);
h1 {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-md);
}
p {
font-size: var(--font-size-lg);
opacity: 0.9;
margin: 0;
}
}
.error-actions {
display: flex;
gap: var(--spacing-md);
justify-content: center;
}
@media (max-width: 640px) {
.error-number {
font-size: 8rem;
}
.error-actions {
flex-direction: column;
align-items: center;
}
}
</style>

558
src/views/Profile.vue Normal file
View File

@@ -0,0 +1,558 @@
<template>
<div class="profile-page">
<div class="container">
<div class="page-header">
<h1>个人资料</h1>
<p>管理您的账户信息和偏好设置</p>
</div>
<div class="profile-content">
<!-- 基本信息 -->
<div class="profile-section">
<h2>基本信息</h2>
<div class="info-card">
<div class="avatar-section">
<n-avatar
size="large"
:src="userInfo.avatar"
fallback-src="/icons/xiaoyugan.png"
/>
<n-button
size="small"
@click="changeAvatar"
>
更换头像
</n-button>
</div>
<div class="info-form">
<n-form
:model="userInfo"
label-placement="left"
label-width="80px"
>
<n-form-item label="用户名">
<n-input
v-model:value="userInfo.username"
readonly
/>
</n-form-item>
<n-form-item label="邮箱">
<n-input v-model:value="userInfo.email" />
</n-form-item>
<n-form-item label="昵称">
<n-input
v-model:value="userInfo.nickname"
placeholder="请输入昵称"
/>
</n-form-item>
<n-form-item label="手机">
<n-input
v-model:value="userInfo.phone"
placeholder="请输入手机号"
/>
</n-form-item>
</n-form>
<div class="form-actions">
<n-button
type="primary"
@click="saveProfile"
>
保存更改
</n-button>
</div>
</div>
</div>
</div>
<!-- 密码修改 -->
<div class="profile-section">
<h2>密码修改</h2>
<div class="info-card">
<n-form
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
label-placement="left"
label-width="100px"
>
<n-form-item
label="当前密码"
path="currentPassword"
>
<n-input
v-model:value="passwordForm.currentPassword"
type="password"
placeholder="请输入当前密码"
/>
</n-form-item>
<n-form-item
label="新密码"
path="newPassword"
>
<n-input
v-model:value="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
/>
</n-form-item>
<n-form-item
label="确认新密码"
path="confirmPassword"
>
<n-input
v-model:value="passwordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
/>
</n-form-item>
</n-form>
<div class="form-actions">
<n-button
type="primary"
@click="changePassword"
>
修改密码
</n-button>
</div>
</div>
</div>
<!-- 系统偏好 -->
<div class="profile-section">
<h2>系统偏好</h2>
<div class="info-card">
<div class="preferences-grid">
<div class="preference-item">
<div class="preference-label">
<h3>主题设置</h3>
<p>选择您喜欢的界面主题</p>
</div>
<n-select
v-model:value="preferences.theme"
:options="themeOptions"
@update:value="updateTheme"
/>
</div>
<div class="preference-item">
<div class="preference-label">
<h3>语言设置</h3>
<p>选择界面显示语言</p>
</div>
<n-select
v-model:value="preferences.language"
:options="languageOptions"
/>
</div>
<div class="preference-item">
<div class="preference-label">
<h3>通知设置</h3>
<p>接收任务完成通知</p>
</div>
<n-switch v-model:value="preferences.notifications" />
</div>
<div class="preference-item">
<div class="preference-label">
<h3>自动执行</h3>
<p>默认开启任务自动执行</p>
</div>
<n-switch v-model:value="preferences.autoExecute" />
</div>
</div>
<div class="form-actions">
<n-button
type="primary"
@click="savePreferences"
>
保存偏好
</n-button>
</div>
</div>
</div>
<!-- Token管理 -->
<div class="profile-section">
<h2>Token管理</h2>
<TokenManager />
</div>
<!-- 账户安全 -->
<div class="profile-section">
<h2>账户安全</h2>
<div class="info-card">
<div class="security-items">
<div class="security-item">
<div class="security-info">
<h3>两步验证</h3>
<p>为您的账户添加额外的安全保护</p>
</div>
<n-button @click="setupTwoFactor">
设置
</n-button>
</div>
<div class="security-item">
<div class="security-info">
<h3>登录历史</h3>
<p>查看最近的登录记录</p>
</div>
<n-button @click="viewLoginHistory">
查看
</n-button>
</div>
<div class="security-item">
<div class="security-info">
<h3>数据导出</h3>
<p>导出您的所有数据</p>
</div>
<n-button @click="exportData">
导出
</n-button>
</div>
<div class="security-item danger">
<div class="security-info">
<h3>删除账户</h3>
<p>永久删除您的账户和所有数据</p>
</div>
<n-button
type="error"
@click="deleteAccount"
>
删除
</n-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage, useDialog } from 'naive-ui'
import { useAuthStore } from '@/stores/auth'
import TokenManager from '@/components/TokenManager.vue'
const router = useRouter()
const message = useMessage()
const dialog = useDialog()
const authStore = useAuthStore()
const passwordFormRef = ref(null)
// 用户信息
const userInfo = reactive({
username: '',
email: '',
nickname: '',
phone: '',
avatar: ''
})
// 密码表单
const passwordForm = reactive({
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
// 系统偏好
const preferences = reactive({
theme: 'auto',
language: 'zh-CN',
notifications: true,
autoExecute: false
})
// 密码验证规则
const passwordRules = {
currentPassword: [
{ required: true, message: '请输入当前密码', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{
validator: (rule, value) => value === passwordForm.newPassword,
message: '两次输入的密码不一致',
trigger: 'blur'
}
]
}
// 选项数据
const themeOptions = [
{ label: '跟随系统', value: 'auto' },
{ label: '浅色主题', value: 'light' },
{ label: '深色主题', value: 'dark' }
]
const languageOptions = [
{ label: '简体中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' }
]
// 方法
const saveProfile = async () => {
try {
// 这里应该调用API保存用户信息
message.success('个人信息保存成功')
} catch (error) {
message.error('保存失败,请稍后重试')
}
}
const changePassword = async () => {
if (!passwordFormRef.value) return
try {
await passwordFormRef.value.validate()
// 这里应该调用API修改密码
message.success('密码修改成功')
// 清空表单
Object.keys(passwordForm).forEach(key => {
passwordForm[key] = ''
})
} catch (error) {
// 验证失败
}
}
const savePreferences = () => {
// 保存偏好设置
localStorage.setItem('userPreferences', JSON.stringify(preferences))
message.success('偏好设置保存成功')
}
const updateTheme = (theme) => {
preferences.theme = theme
// 这里可以实际应用主题变更
}
const changeAvatar = () => {
message.info('头像更换功能开发中...')
}
const setupTwoFactor = () => {
message.info('两步验证设置功能开发中...')
}
const viewLoginHistory = () => {
message.info('登录历史查看功能开发中...')
}
const exportData = () => {
message.info('数据导出功能开发中...')
}
const deleteAccount = () => {
dialog.warning({
title: '删除账户',
content: '此操作将永久删除您的账户和所有数据,且无法恢复。确定要继续吗?',
positiveText: '确定删除',
negativeText: '取消',
onPositiveClick: () => {
message.error('账户删除功能暂未开放')
}
})
}
// 生命周期
onMounted(() => {
// 加载用户信息
if (authStore.userInfo) {
Object.assign(userInfo, authStore.userInfo)
}
// 加载用户偏好
const savedPreferences = localStorage.getItem('userPreferences')
if (savedPreferences) {
try {
Object.assign(preferences, JSON.parse(savedPreferences))
} catch (error) {
console.error('解析用户偏好失败:', error)
}
}
})
</script>
<style scoped lang="scss">
.profile-page {
min-height: 100vh;
background: var(--bg-secondary);
padding: var(--spacing-xl) 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 var(--spacing-lg);
}
.page-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
h1 {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
p {
color: var(--text-secondary);
font-size: var(--font-size-lg);
margin: 0;
}
}
.profile-section {
margin-bottom: var(--spacing-2xl);
h2 {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
}
}
.info-card {
background: white;
border-radius: var(--border-radius-large);
padding: var(--spacing-xl);
box-shadow: var(--shadow-light);
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
padding-bottom: var(--spacing-xl);
border-bottom: 1px solid var(--border-light);
}
.info-form {
.n-form-item {
margin-bottom: var(--spacing-lg);
}
}
.form-actions {
margin-top: var(--spacing-xl);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-light);
}
.preferences-grid {
display: grid;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
.preference-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-lg);
}
.preference-label {
flex: 1;
h3 {
font-size: var(--font-size-md);
font-weight: var(--font-weight-medium);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin: 0;
}
}
.security-items {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.security-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-lg);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-medium);
transition: all var(--transition-fast);
&:hover {
box-shadow: var(--shadow-light);
}
&.danger {
border-color: var(--error-color);
background: rgba(208, 48, 80, 0.05);
}
}
.security-info {
flex: 1;
h3 {
font-size: var(--font-size-md);
font-weight: var(--font-weight-medium);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin: 0;
}
}
@media (max-width: 768px) {
.container {
padding: 0 var(--spacing-md);
}
.info-card {
padding: var(--spacing-lg);
}
.preference-item,
.security-item {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-md);
}
.preference-item .n-select,
.preference-item .n-switch {
width: 100%;
}
}
</style>

333
src/views/Register.vue Normal file
View File

@@ -0,0 +1,333 @@
<template>
<div class="register-page">
<div class="register-container">
<div class="register-card glass">
<div class="card-header">
<div class="brand">
<img
src="/icons/logo.png"
alt="XYZW"
class="brand-logo"
>
<h1 class="brand-title">
注册 XYZW 账户
</h1>
</div>
<p class="welcome-text">
加入我们开始您的游戏管理之旅
</p>
</div>
<div class="card-body">
<n-form
ref="registerFormRef"
:model="registerForm"
:rules="registerRules"
size="large"
:show-label="false"
>
<n-form-item path="username">
<n-input
v-model:value="registerForm.username"
placeholder="用户名"
:input-props="{ autocomplete: 'username' }"
>
<template #prefix>
<n-icon><PersonCircle /></n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="email">
<n-input
v-model:value="registerForm.email"
placeholder="邮箱地址"
:input-props="{ autocomplete: 'email' }"
>
<template #prefix>
<n-icon><Mail /></n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="password">
<n-input
v-model:value="registerForm.password"
type="password"
placeholder="密码"
:input-props="{ autocomplete: 'new-password' }"
>
<template #prefix>
<n-icon><Lock /></n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="confirmPassword">
<n-input
v-model:value="registerForm.confirmPassword"
type="password"
placeholder="确认密码"
:input-props="{ autocomplete: 'new-password' }"
@keydown.enter="handleRegister"
>
<template #prefix>
<n-icon><Lock /></n-icon>
</template>
</n-input>
</n-form-item>
<div class="form-options">
<n-checkbox v-model:checked="registerForm.agreeTerms">
我已阅读并同意
<n-button
text
type="primary"
@click="showTerms = true"
>
服务条款
</n-button>
<n-button
text
type="primary"
@click="showPrivacy = true"
>
隐私政策
</n-button>
</n-checkbox>
</div>
<n-button
type="primary"
size="large"
block
:loading="authStore.isLoading"
:disabled="!registerForm.agreeTerms"
class="register-button"
@click="handleRegister"
>
注册账户
</n-button>
</n-form>
<div class="login-prompt">
<span>已有账户</span>
<n-button
text
type="primary"
@click="router.push('/login')"
>
立即登录
</n-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { useAuthStore } from '@/stores/auth'
import { PersonCircle, Lock, Mail } from '@vicons/ionicons5'
const router = useRouter()
const message = useMessage()
const authStore = useAuthStore()
const registerFormRef = ref(null)
// 注册表单数据
const registerForm = reactive({
username: '',
email: '',
password: '',
confirmPassword: '',
agreeTerms: false
})
// 表单验证规则
const registerRules = {
username: [
{
required: true,
message: '请输入用户名',
trigger: ['input', 'blur']
},
{
min: 3,
max: 20,
message: '用户名长度应在3-20个字符之间',
trigger: ['input', 'blur']
}
],
email: [
{
required: true,
message: '请输入邮箱地址',
trigger: ['input', 'blur']
},
{
type: 'email',
message: '请输入正确的邮箱格式',
trigger: ['input', 'blur']
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['input', 'blur']
},
{
min: 6,
message: '密码长度不能少于6位',
trigger: ['input', 'blur']
}
],
confirmPassword: [
{
required: true,
message: '请确认密码',
trigger: ['input', 'blur']
},
{
validator: (rule, value) => {
return value === registerForm.password
},
message: '两次输入的密码不一致',
trigger: ['input', 'blur']
}
]
}
// 处理注册
const handleRegister = async () => {
if (!registerFormRef.value) return
try {
await registerFormRef.value.validate()
if (!registerForm.agreeTerms) {
message.warning('请先同意服务条款和隐私政策')
return
}
const result = await authStore.register({
username: registerForm.username,
email: registerForm.email,
password: registerForm.password
})
if (result.success) {
message.success('注册成功,请登录')
router.push('/login')
} else {
message.error(result.message)
}
} catch (error) {
console.error('Registration validation failed:', error)
}
}
</script>
<style scoped lang="scss">
.register-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-lg);
}
.register-container {
max-width: 500px;
width: 100%;
}
.register-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: var(--border-radius-xl);
padding: var(--spacing-2xl);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-header {
text-align: center;
margin-bottom: var(--spacing-xl);
}
.brand {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.brand-logo {
width: 64px;
height: 64px;
border-radius: var(--border-radius-large);
}
.brand-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin: 0;
}
.welcome-text {
color: var(--text-secondary);
font-size: var(--font-size-md);
margin: 0;
}
.card-body {
.n-form {
.n-form-item {
margin-bottom: var(--spacing-lg);
}
}
}
.form-options {
margin-bottom: var(--spacing-xl);
:deep(.n-checkbox) {
line-height: var(--line-height-relaxed);
}
}
.register-button {
height: 48px;
font-size: var(--font-size-md);
font-weight: var(--font-weight-medium);
margin-bottom: var(--spacing-lg);
}
.login-prompt {
text-align: center;
color: var(--text-secondary);
span {
margin-right: var(--spacing-sm);
}
}
@media (max-width: 640px) {
.register-card {
padding: var(--spacing-xl);
}
.brand-title {
font-size: var(--font-size-xl);
}
}
</style>

1044
src/views/TokenImport.vue Normal file

File diff suppressed because it is too large Load Diff