fix: 增加用户面板等功能,功能尚存缺陷

refactor
Lxy 6 days ago
parent 76a98d3b95
commit 9e8306be3b

Binary file not shown.

@ -0,0 +1,415 @@
<template>
<div v-if="props.visible" class="user-panel">
<div class="panel-content">
<!-- 用户信息 -->
<div class="user-section">
<div class="user-avatar">{{ authStore.user?.username?.charAt(0)?.toUpperCase() || 'U' }}</div>
<div class="user-details">
<div class="user-name">{{ authStore.user?.username }}</div>
<div class="user-role" :class="authStore.user?.role">
{{ authStore.user?.role === 'admin' ? '管理员' : '普通用户' }}
</div>
</div>
<button class="close-btn" @click="handleClose"></button>
</div>
<!-- 用户统计 -->
<div class="user-stats">
<div class="stat-item">
<span class="stat-num">{{ plans.length }}</span>
<span class="stat-label">行程规划</span>
</div>
<div class="stat-item">
<span class="stat-num">0</span>
<span class="stat-label">行程收藏</span>
</div>
<div class="stat-item">
<span class="stat-num">0</span>
<span class="stat-label">地点收藏</span>
</div>
</div>
<!-- Tab 导航 -->
<div class="tab-nav">
<button :class="{ active: activeTab === 'plans' }" @click="activeTab = 'plans'">📋 规划记录</button>
<button :class="{ active: activeTab === 'favorites' }" @click="activeTab = 'favorites'"> 行程收藏</button>
<button :class="{ active: activeTab === 'locations' }" @click="activeTab = 'locations'">📍 地点收藏</button>
</div>
<!-- 规划记录 -->
<div v-if="activeTab === 'plans'" class="tab-content">
<div v-if="loadingPlans" class="loading">...</div>
<div v-else-if="plans.length === 0" class="empty">暂无规划记录</div>
<div v-else class="plan-list">
<div v-for="plan in plans" :key="plan.id" class="plan-item" @click="loadPlan(plan)">
<div class="plan-name">{{ plan.name }}</div>
<div v-if="plan.description" class="plan-desc">{{ plan.description }}</div>
<div class="plan-date">{{ formatDate(plan.created_at) }}</div>
</div>
</div>
</div>
<!-- 行程收藏 -->
<div v-if="activeTab === 'favorites'" class="tab-content">
<div class="empty-placeholder">
<div class="empty-icon"></div>
<p>暂无收藏行程</p>
<p class="hint">功能开发中...</p>
</div>
</div>
<!-- 地点收藏 -->
<div v-if="activeTab === 'locations'" class="tab-content">
<div class="empty-placeholder">
<div class="empty-icon">📍</div>
<p>暂无收藏地点</p>
<p class="hint">功能开发中...</p>
</div>
</div>
<!-- 退出登录按钮 -->
<div class="logout-section">
<button class="logout-panel-btn" @click="handleLogout">🚪 退</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useItineraryStore } from '../stores/itinerary'
import { trackPlanLoad } from '../services/statsService'
const props = defineProps({
visible: { type: Boolean, default: false }
})
const emit = defineEmits(['update:visible', 'load', 'close'])
const router = useRouter()
const authStore = useAuthStore()
const itineraryStore = useItineraryStore()
const activeTab = ref('plans')
const plans = ref([])
const loadingPlans = ref(false)
onMounted(async () => {
if (authStore.isAuthenticated) {
await loadPlans()
}
})
function handleClose() {
emit('update:visible', false)
emit('close')
}
function handleLogout() {
if (confirm('确定要退出登录吗?')) {
authStore.logout()
emit('update:visible', false)
router.push('/')
}
}
async function loadPlans() {
loadingPlans.value = true
try {
const API_BASE = 'http://localhost:3001/api'
const headers = { 'Authorization': `Bearer ${authStore.token}` }
const res = await fetch(`${API_BASE}/plans`, { headers })
const data = await res.json()
plans.value = data.plans || []
} catch (err) {
console.error('加载规划记录失败:', err)
} finally {
loadingPlans.value = false
}
}
async function loadPlan(plan) {
loadingPlans.value = true
try {
const API_BASE = 'http://localhost:3001/api'
const headers = { 'Authorization': `Bearer ${authStore.token}` }
const res = await fetch(`${API_BASE}/plans/${plan.id}`, { headers })
const data = await res.json()
if (data.plan) {
const planData = data.plan.data
if (planData.points) {
itineraryStore.loadFromAI({ points: planData.points })
itineraryStore.setPhase('workbench')
trackPlanLoad(plan.id)
emit('load', plan)
router.push('/')
}
}
} catch (err) {
console.error('加载行程失败:', err)
alert('加载行程失败')
} finally {
loadingPlans.value = false
}
}
function formatDate(dateStr) {
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
}
</script>
<style scoped>
.user-panel {
position: fixed;
top: 60px;
right: 0;
width: 320px;
height: calc(100vh - 60px);
background: #fff;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
z-index: 9998;
display: flex;
flex-direction: column;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
}
/* 用户信息 */
.user-section {
position: relative;
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid #e9ecef;
margin-bottom: 16px;
}
.user-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
flex-shrink: 0;
}
.user-details {
flex: 1;
min-width: 0;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #2d3436;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
display: inline-block;
margin-top: 4px;
}
.user-role.admin {
background: #ffe0e0;
color: #d63031;
}
.user-role.user {
background: #e8e4ff;
color: #6c5ce7;
}
/* 关闭按钮 */
.close-btn {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: #f5f7fa;
color: #636e72;
border: none;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.close-btn:hover {
background: #e8e4ff;
color: #6c5ce7;
}
/* Tab 导航 */
.tab-nav {
display: flex;
gap: 4px;
margin-bottom: 16px;
}
.tab-nav button {
flex: 1;
padding: 8px 4px;
border: none;
background: #f5f7fa;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
color: #636e72;
}
.tab-nav button.active {
background: #6c5ce7;
color: #fff;
}
.tab-nav button:hover:not(.active) {
background: #e8e4ff;
}
/* Tab 内容 */
.tab-content {
flex: 1;
min-height: 150px;
}
/* 用户统计 */
.user-stats {
display: flex;
justify-content: space-around;
padding: 16px 0;
border-bottom: 1px solid #e9ecef;
margin-bottom: 16px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.stat-num {
font-size: 20px;
font-weight: 700;
color: #6c5ce7;
}
.stat-label {
font-size: 11px;
color: #636e72;
}
.loading,
.empty {
text-align: center;
color: #b2bec3;
padding: 40px 0;
}
.empty-placeholder {
text-align: center;
padding: 40px 0;
color: #b2bec3;
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
.empty-placeholder .hint {
font-size: 12px;
color: #dfe6e9;
margin-top: 8px;
}
/* 规划列表 */
.plan-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.plan-item {
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.plan-item:hover {
background: #e8e4ff;
}
.plan-name {
font-size: 13px;
font-weight: 600;
color: #2d3436;
margin-bottom: 4px;
}
.plan-desc {
font-size: 11px;
color: #636e72;
margin-bottom: 6px;
line-height: 1.3;
}
.plan-date {
font-size: 10px;
color: #b2bec3;
}
/* 退出登录区域 - 自动推到底部 */
.logout-section {
margin-top: auto;
padding-top: 16px;
border-top: 1px solid #e9ecef;
}
.logout-panel-btn {
width: 100%;
padding: 12px;
border-radius: 8px;
border: 2px solid #d63031;
background: #fff;
color: #d63031;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.logout-panel-btn:hover {
background: #d63031;
color: #fff;
}
</style>

@ -5,16 +5,18 @@
<div class="brand"><span class="icon"></span> 智能行程规划</div>
<div class="top-bar-right">
<button v-if="authStore.isAdmin" class="admin-btn" @click="$router.push('/admin')"></button>
<button v-if="authStore.isAuthenticated" class="history-btn" @click="showHistory = true">📋 </button>
<div v-if="authStore.isAuthenticated" class="user-info">
<span class="username">{{ authStore.user?.username }}</span>
<button class="logout-btn" @click="handleLogout">退</button>
</div>
<div v-else-if="authStore.isGuest" class="guest-info">
<span class="guest-badge">👤 游客</span>
<button class="login-btn" @click="showAuthModal = true">登录/注册</button>
</div>
<button v-else class="login-btn" @click="showAuthModal = true">登录/注册</button>
<button v-if="authStore.isAuthenticated" class="user-toggle-btn" @click="toggleUserProfile">
<span class="user-avatar-small">{{ authStore.user?.username?.charAt(0)?.toUpperCase() }}</span>
<span class="user-name-btn">{{ authStore.user?.username }}</span>
<span class="toggle-arrow">{{ showUserProfile ? '▲' : '▼' }}</span>
</button>
<template v-else>
<button class="user-toggle-btn guest" @click="showHistory = true">
<span class="user-avatar-small">👤</span>
<span class="user-name-btn">游客</span>
</button>
<button class="login-small-btn" @click="showAuthModal = true">登录</button>
</template>
<button v-if="!showModeSelection" class="home-btn" @click="goHome"></button>
</div>
</nav>
@ -57,8 +59,11 @@
<Workbench v-else />
</div>
<!-- 历史记录面板 -->
<HistoryPanel v-if="showHistory" @close="showHistory = false" @load="showHistory = false" />
<!-- 用户管理面板已登录用户 -->
<UserProfilePanel v-if="authStore.isAuthenticated" v-model:visible="showUserProfile" @load="showUserProfile = false" />
<!-- 历史记录面板游客 -->
<HistoryPanel v-if="!authStore.isAuthenticated && showHistory" @close="showHistory = false" @load="showHistory = false" />
<!-- 登录/注册模态框 -->
<AuthModal v-model:visible="showAuthModal" @success="handleAuthSuccess" />
@ -67,7 +72,6 @@
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useItineraryStore } from '../stores/itinerary'
import { useAuthStore } from '../stores/auth'
import ChatInterface from '../components/ChatInterface.vue'
@ -77,11 +81,12 @@ import QuickPlanPanel from '../components/QuickPlanPanel.vue'
import CustomPlanPanel from '../components/CustomPlanPanel.vue'
import HistoryPanel from '../components/HistoryPanel.vue'
import AuthModal from '../components/AuthModal.vue'
import UserProfilePanel from '../components/UserProfilePanel.vue'
const store = useItineraryStore()
const authStore = useAuthStore()
const router = useRouter()
const showHistory = ref(false)
const showUserProfile = ref(false)
const showAuthModal = ref(false)
const showModeSelection = computed(() => {
@ -96,12 +101,13 @@ function handleAuthSuccess() {
}
}
function handleLogout() {
if (confirm('确定要退出登录吗?')) {
authStore.logout()
//
authStore.initGuestSession()
router.push('/')
function toggleUserProfile() {
if (!showUserProfile.value) {
//
showUserProfile.value = true
} else {
//
showUserProfile.value = false
}
}
@ -159,81 +165,81 @@ const goHome = () => {
gap: 12px;
}
.login-btn {
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
.user-toggle-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 20px;
border: 1.5px solid #6c5ce7;
background: #fff;
color: #6c5ce7;
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
font-weight: 500;
color: #2d3436;
}
.login-btn:hover {
background: #6c5ce7;
color: #fff;
.user-toggle-btn:hover {
background: #f8f9ff;
border-color: #5b4bd5;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
.user-toggle-btn.guest {
border-color: #b2bec3;
color: #636e72;
}
.guest-info {
display: flex;
align-items: center;
gap: 8px;
.user-toggle-btn.guest:hover {
background: #f5f7fa;
}
.guest-badge {
font-size: 12px;
color: #636e72;
background: #f5f7fa;
padding: 4px 10px;
.login-small-btn {
padding: 6px 14px;
border-radius: 20px;
font-weight: 500;
border: 1.5px solid #6c5ce7;
background: #6c5ce7;
color: #fff;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.username {
font-size: 13px;
color: #2d3436;
font-weight: 600;
.login-small-btn:hover {
background: #5b4bd5;
border-color: #5b4bd5;
}
.logout-btn {
padding: 6px 12px;
border-radius: 6px;
.user-avatar-small {
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
cursor: pointer;
border: 1.5px solid #d63031;
background: #fff;
color: #d63031;
transition: all 0.2s;
font-weight: 700;
}
.logout-btn:hover {
background: #d63031;
color: #fff;
.user-toggle-btn.guest .user-avatar-small {
background: #dfe6e9;
font-size: 14px;
}
.history-btn {
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
border: 1.5px solid #00b894;
background: #fff;
color: #00b894;
transition: all 0.2s;
font-weight: 500;
.user-name-btn {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-btn:hover {
background: #00b894;
color: #fff;
.toggle-arrow {
font-size: 10px;
color: #b2bec3;
margin-left: 4px;
}
.admin-btn {

Loading…
Cancel
Save