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>
|
||||
Reference in New Issue
Block a user