Spaces:
Running
Running
| <template> | |
| <div class="page-container"> | |
| <div class="title-container"> | |
| <span class="main-title">Top Asset Requests</span> | |
| <p class="subtitle">Community requested assets ranked by votes</p> | |
| </div> | |
| <div class="requests-container"> | |
| <Card class="requests-card"> | |
| <template #content> | |
| <div v-if="loading" class="loading-state"> | |
| <ProgressSpinner /> | |
| <p>Loading requests...</p> | |
| </div> | |
| <div v-else-if="topRequests.length === 0" class="empty-state"> | |
| <i class="pi pi-inbox" style="font-size: 3rem; color: #9ca3af;"></i> | |
| <p>No asset requests yet. Be the first to request one!</p> | |
| <Button label="Request Asset" @click="$router.push('/add-asset')" /> | |
| </div> | |
| <div v-else class="requests-list"> | |
| <div | |
| v-for="(request, index) in topRequests" | |
| :key="index" | |
| class="request-item" | |
| :class="{ 'top-rank': index < 3 }" | |
| > | |
| <div class="rank-badge" :class="`rank-${index + 1}`"> | |
| {{ index + 1 }} | |
| </div> | |
| <div class="request-content"> | |
| <div class="request-header"> | |
| <span class="asset-symbol">{{ request.symbol }}</span> | |
| <span class="asset-type-badge" :class="`type-${request.type}`"> | |
| {{ request.type }} | |
| </span> | |
| </div> | |
| <div v-if="request.reason" class="request-reason"> | |
| {{ request.reason }} | |
| </div> | |
| <div class="request-meta"> | |
| <span class="timestamp"> | |
| <i class="pi pi-clock"></i> | |
| {{ formatDate(request.timestamp) }} | |
| </span> | |
| </div> | |
| </div> | |
| <div class="vote-section"> | |
| <Button | |
| icon="pi pi-thumbs-up" | |
| :label="String(request.votes)" | |
| @click="voteForAsset(request.symbol)" | |
| class="vote-button" | |
| :disabled="hasVoted(request.symbol)" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="actions-footer"> | |
| <Button | |
| label="Back to Submissions" | |
| icon="pi pi-arrow-left" | |
| @click="$router.push('/add-asset')" | |
| outlined | |
| /> | |
| </div> | |
| </template> | |
| </Card> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| import Card from 'primevue/card' | |
| import Button from 'primevue/button' | |
| import ProgressSpinner from 'primevue/progressspinner' | |
| import { supabase } from '../lib/supabase.js' | |
| export default { | |
| name: 'AssetRequestsView', | |
| components: { | |
| Card, | |
| Button, | |
| ProgressSpinner | |
| }, | |
| data() { | |
| return { | |
| loading: true, | |
| topRequests: [], | |
| votedAssets: new Set() | |
| } | |
| }, | |
| mounted() { | |
| this.loadRequests() | |
| this.loadVotedAssets() | |
| }, | |
| methods: { | |
| async loadRequests() { | |
| this.loading = true | |
| try { | |
| const { data: row, error } = await supabase | |
| .from('trading_decisions') | |
| .select('asset_requests_data') | |
| .eq('agent_name', 'InvestorAgent') | |
| .eq('date', '2025-08-01') | |
| .eq('asset', 'BTC') | |
| .eq('model', 'gpt_4.1') | |
| .single() | |
| if (error) { | |
| console.error('Error loading requests:', error) | |
| return | |
| } | |
| const requests = row?.asset_requests_data || [] | |
| // 按投票数排序,取前10个 | |
| this.topRequests = requests | |
| .sort((a, b) => b.votes - a.votes) | |
| .slice(0, 10) | |
| } catch (e) { | |
| console.error('Error loading requests:', e) | |
| } finally { | |
| this.loading = false | |
| } | |
| }, | |
| loadVotedAssets() { | |
| try { | |
| const stored = localStorage.getItem('votedAssets') | |
| if (stored) { | |
| this.votedAssets = new Set(JSON.parse(stored)) | |
| } | |
| } catch (e) { | |
| console.error('Error loading voted assets:', e) | |
| } | |
| }, | |
| async voteForAsset(symbol) { | |
| try { | |
| // 读取现有数据 | |
| const { data: row, error: fetchError } = await supabase | |
| .from('trading_decisions') | |
| .select('asset_requests_data') | |
| .eq('agent_name', 'InvestorAgent') | |
| .eq('date', '2025-08-01') | |
| .eq('asset', 'BTC') | |
| .single() | |
| if (fetchError) throw fetchError | |
| const requests = row?.asset_requests_data || [] | |
| const index = requests.findIndex(r => r.symbol === symbol) | |
| if (index >= 0) { | |
| requests[index].votes += 1 | |
| requests[index].timestamp = new Date().toISOString() | |
| // 保存回 Supabase | |
| const { error: updateError } = await supabase | |
| .from('trading_decisions') | |
| .update({ asset_requests_data: requests }) | |
| .eq('agent_name', 'InvestorAgent') | |
| .eq('date', '2025-08-01') | |
| .eq('asset', 'BTC') | |
| .eq('model', 'gpt_4.1') | |
| if (updateError) throw updateError | |
| // 记录投票 | |
| this.votedAssets.add(symbol) | |
| localStorage.setItem('votedAssets', JSON.stringify([...this.votedAssets])) | |
| // 重新加载 | |
| await this.loadRequests() | |
| } | |
| } catch (e) { | |
| console.error('Error voting:', e) | |
| } | |
| }, | |
| hasVoted(symbol) { | |
| return this.votedAssets.has(symbol.toUpperCase()) | |
| }, | |
| formatDate(timestamp) { | |
| try { | |
| const date = new Date(timestamp) | |
| const now = new Date() | |
| const diff = now - date | |
| const minutes = Math.floor(diff / 60000) | |
| const hours = Math.floor(diff / 3600000) | |
| const days = Math.floor(diff / 86400000) | |
| if (minutes < 1) return 'Just now' | |
| if (minutes < 60) return `${minutes}m ago` | |
| if (hours < 24) return `${hours}h ago` | |
| if (days < 7) return `${days}d ago` | |
| return date.toLocaleDateString() | |
| } catch (e) { | |
| return 'Recently' | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| .page-container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 1rem; | |
| } | |
| .title-container { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .main-title { | |
| font-size: 2rem; | |
| letter-spacing: -0.02em; | |
| font-weight: 800; | |
| color: #1f1f33; | |
| } | |
| .subtitle { | |
| color: #6b7280; | |
| margin-top: 0.5rem; | |
| font-size: 1rem; | |
| } | |
| .requests-card { | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); | |
| } | |
| .loading-state, | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 3rem; | |
| gap: 1rem; | |
| color: #6b7280; | |
| } | |
| .requests-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .request-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| padding: 1.25rem; | |
| border: 2px solid #e5e7eb; | |
| border-radius: 12px; | |
| transition: all 0.2s; | |
| background: #fafafa; | |
| } | |
| .request-item:hover { | |
| border-color: #d1d5db; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | |
| } | |
| .request-item.top-rank { | |
| background: linear-gradient(135deg, #fef3c7 0%, #fef9f3 100%); | |
| border-color: #fbbf24; | |
| } | |
| .rank-badge { | |
| min-width: 48px; | |
| height: 48px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| font-weight: 900; | |
| border-radius: 50%; | |
| background: #e5e7eb; | |
| color: #4b5563; | |
| } | |
| .rank-badge.rank-1 { | |
| background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); | |
| color: white; | |
| box-shadow: 0 4px 12px rgba(251, 191, 36, 0.4); | |
| } | |
| .rank-badge.rank-2 { | |
| background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%); | |
| color: white; | |
| } | |
| .rank-badge.rank-3 { | |
| background: linear-gradient(135deg, #d97706 0%, #92400e 100%); | |
| color: white; | |
| } | |
| .request-content { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .request-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .asset-symbol { | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| color: #1f1f33; | |
| } | |
| .asset-type-badge { | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 999px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .type-stock { | |
| background: #dbeafe; | |
| color: #1e40af; | |
| } | |
| .type-crypto { | |
| background: #fef3c7; | |
| color: #92400e; | |
| } | |
| .type-etf { | |
| background: #d1fae5; | |
| color: #065f46; | |
| } | |
| .type-other { | |
| background: #f3f4f6; | |
| color: #4b5563; | |
| } | |
| .request-reason { | |
| color: #6b7280; | |
| font-size: 0.9rem; | |
| margin-bottom: 0.5rem; | |
| line-height: 1.5; | |
| } | |
| .request-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| font-size: 0.875rem; | |
| color: #9ca3af; | |
| } | |
| .timestamp { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.25rem; | |
| } | |
| .vote-section { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .vote-button { | |
| min-width: 80px; | |
| } | |
| :deep(.vote-button .p-button-label) { | |
| font-weight: 700; | |
| font-size: 1rem; | |
| } | |
| .actions-footer { | |
| margin-top: 2rem; | |
| padding-top: 1.5rem; | |
| border-top: 1px solid #e5e7eb; | |
| display: flex; | |
| justify-content: center; | |
| } | |
| @media (max-width: 768px) { | |
| .main-title { | |
| font-size: 1.5rem; | |
| } | |
| .request-item { | |
| flex-direction: column; | |
| text-align: center; | |
| } | |
| .request-header { | |
| justify-content: center; | |
| } | |
| } | |
| </style> | |