Initial public release
This commit is contained in:
759
src/views/DailyTasks.vue
Normal file
759
src/views/DailyTasks.vue
Normal 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
840
src/views/Dashboard.vue
Normal 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
570
src/views/GameFeatures.vue
Normal 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
575
src/views/GameRoles.vue
Normal 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
605
src/views/Home.vue
Normal 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>© 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
554
src/views/Login.vue
Normal 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
116
src/views/NotFound.vue
Normal 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
558
src/views/Profile.vue
Normal 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
333
src/views/Register.vue
Normal 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
1044
src/views/TokenImport.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user