新增:活动模块

This commit is contained in:
马丁 2026-04-27 17:45:14 +08:00
parent 07291abcf7
commit 0d523ee5b8
10 changed files with 344 additions and 9316 deletions

9315
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ const currentRoute = computed(() => route.path)
const showBottomNav = computed(() => { const showBottomNav = computed(() => {
if (!display.smAndDown.value) return false if (!display.smAndDown.value) return false
return currentRoute.value === '/' || currentRoute.value === '/search' || currentRoute.value === '/profile' return currentRoute.value === '/' || currentRoute.value === '/search' || currentRoute.value === '/earn-activity' || currentRoute.value === '/profile'
}) })
const mineTargetPath = computed(() => const mineTargetPath = computed(() =>
userStore.isLoggedIn ? '/profile' : '/login', userStore.isLoggedIn ? '/profile' : '/login',
@ -35,6 +35,7 @@ const bottomNavValue = computed({
const p = currentRoute.value const p = currentRoute.value
if (p.startsWith('/profile') || p === '/login') return '/profile' if (p.startsWith('/profile') || p === '/login') return '/profile'
if (p.startsWith('/search')) return '/search' if (p.startsWith('/search')) return '/search'
if (p.startsWith('/earn-activity')) return '/earn-activity'
return '/' return '/'
}, },
set(v: string) { set(v: string) {
@ -177,6 +178,10 @@ watch(
<v-icon size="24">mdi-magnify</v-icon> <v-icon size="24">mdi-magnify</v-icon>
<span>{{ t('nav.search') }}</span> <span>{{ t('nav.search') }}</span>
</v-btn> </v-btn>
<v-btn value="/earn-activity" :ripple="false">
<v-icon size="24">mdi-lock-outline</v-icon>
<span>{{ t('nav.activity') }}</span>
</v-btn>
<v-btn value="/profile" :ripple="false"> <v-btn value="/profile" :ripple="false">
<v-icon size="24">mdi-account-outline</v-icon> <v-icon size="24">mdi-account-outline</v-icon>
<span>{{ t('nav.mine') }}</span> <span>{{ t('nav.mine') }}</span>

26
src/api/earnActivity.ts Normal file
View File

@ -0,0 +1,26 @@
import { get } from './request'
import type { ApiResponse } from './types'
/** 赚钱活动项polymarket.PmEarnActivity */
export interface PmEarnActivityItem {
ID: number
typeId: number | null
seconds: number | null
moreProfitRate: number | null
minAmount: number | null
[key: string]: unknown // 保留其他可能存在的后端字段
}
export interface GetPmEarnActivityPublicResponse extends ApiResponse<PmEarnActivityItem[]> {
code: number
data?: PmEarnActivityItem[]
msg: string
}
/**
* GET /pmEarnActivity/getPmEarnActivityPublic
*
*/
export async function getPmEarnActivityPublic(): Promise<GetPmEarnActivityPublicResponse> {
return get<GetPmEarnActivityPublicResponse>('/pmEarnActivity/getPmEarnActivityPublic')
}

View File

@ -2,6 +2,7 @@
"nav": { "nav": {
"home": "Home", "home": "Home",
"search": "Search", "search": "Search",
"activity": "Activity",
"mine": "Mine" "mine": "Mine"
}, },
"common": { "common": {
@ -431,6 +432,19 @@
"connectingWallet": "Connecting wallet…", "connectingWallet": "Connecting wallet…",
"walletMismatch": "Switch your browser wallet to the same account you used to sign in." "walletMismatch": "Switch your browser wallet to the same account you used to sign in."
}, },
"earnActivity": {
"title": "Earn Liquidity",
"subtitle": "Participate to earn extra returns",
"duration": "Duration",
"unlimited": "Unlimited",
"extraProfitRate": "Extra Profit Rate",
"notice": "This page is for viewing only. Please use API for locking operations.",
"tiersTitle": "Profit Tiers",
"minAmount": "Min Amount (USDC)",
"tierLevel": "Tier {n}",
"expectedProfit": "Expected Profit",
"days": "{n} Days"
},
"locale": { "locale": {
"zh": "简体中文", "zh": "简体中文",
"en": "English", "en": "English",

View File

@ -2,6 +2,7 @@
"nav": { "nav": {
"home": "ホーム", "home": "ホーム",
"search": "検索", "search": "検索",
"activity": "アクティビティ",
"mine": "マイ" "mine": "マイ"
}, },
"common": { "common": {
@ -431,6 +432,19 @@
"connectingWallet": "ウォレットに接続しています…", "connectingWallet": "ウォレットに接続しています…",
"walletMismatch": "ログイン時に使用したアカウントにブラウザのウォレットを切り替えてください。" "walletMismatch": "ログイン時に使用したアカウントにブラウザのウォレットを切り替えてください。"
}, },
"earnActivity": {
"title": "流動性を稼いでロック",
"subtitle": "キャンペーンに参加して追加收益を獲得",
"duration": "期間",
"unlimited": "無制限",
"extraProfitRate": "追加利益率",
"notice": "このペンは表示のみを目的としています。ロック操作はAPIを通じて行ってください。",
"tiersTitle": "利益ティア",
"minAmount": "最小金額 (USDC)",
"tierLevel": "ティア {n}",
"expectedProfit": "予想利益",
"days": "{n} 日"
},
"locale": { "locale": {
"zh": "简体中文", "zh": "简体中文",
"en": "English", "en": "English",

View File

@ -2,6 +2,7 @@
"nav": { "nav": {
"home": "홈", "home": "홈",
"search": "검색", "search": "검색",
"activity": "활동",
"mine": "마이" "mine": "마이"
}, },
"common": { "common": {
@ -431,6 +432,19 @@
"connectingWallet": "지갑 연결 중…", "connectingWallet": "지갑 연결 중…",
"walletMismatch": "로그인에 사용한 계정과 동일한 계정으로 브라우저 지갑을 전환해 주세요." "walletMismatch": "로그인에 사용한 계정과 동일한 계정으로 브라우저 지갑을 전환해 주세요."
}, },
"earnActivity": {
"title": "유동성 획득 잠금",
"subtitle": "이벤트에 참여하여 추가 수익 획득",
"duration": "기간",
"unlimited": "무제한",
"extraProfitRate": "추가 이익률",
"notice": "이 페이지는 조회 전용입니다. 잠금 작업은 API를 통해 진행해 주세요.",
"tiersTitle": "수익 티어",
"minAmount": "최소 금액 (USDC)",
"tierLevel": "티어 {n}",
"expectedProfit": "예상 수익",
"days": "{n}일"
},
"locale": { "locale": {
"zh": "简体中文", "zh": "简体中文",
"en": "English", "en": "English",

View File

@ -2,6 +2,7 @@
"nav": { "nav": {
"home": "首页", "home": "首页",
"search": "搜索", "search": "搜索",
"activity": "活动",
"mine": "我的" "mine": "我的"
}, },
"common": { "common": {
@ -443,6 +444,19 @@
"connectingWallet": "正在连接钱包…", "connectingWallet": "正在连接钱包…",
"walletMismatch": "请在浏览器钱包中切换到您登录时使用的账户。" "walletMismatch": "请在浏览器钱包中切换到您登录时使用的账户。"
}, },
"earnActivity": {
"title": "锁仓赚取流动性",
"subtitle": "参与活动获得额外收益",
"duration": "活动时长",
"unlimited": "不限时",
"extraProfitRate": "额外利润比例",
"notice": "此页面仅供查看,锁仓操作请通过 API 进行。",
"tiersTitle": "收益挡位",
"minAmount": "最小金额 (USDC)",
"tierLevel": "挡位 {n}",
"expectedProfit": "预期利润",
"days": "{n} 天"
},
"locale": { "locale": {
"zh": "简体中文", "zh": "简体中文",
"en": "English", "en": "English",

View File

@ -2,6 +2,7 @@
"nav": { "nav": {
"home": "首頁", "home": "首頁",
"search": "搜尋", "search": "搜尋",
"activity": "活動",
"mine": "我的" "mine": "我的"
}, },
"common": { "common": {
@ -431,6 +432,19 @@
"connectingWallet": "正在連接錢包…", "connectingWallet": "正在連接錢包…",
"walletMismatch": "請在瀏覽器錢包中切換至您登入時使用的帳戶。" "walletMismatch": "請在瀏覽器錢包中切換至您登入時使用的帳戶。"
}, },
"earnActivity": {
"title": "鎖倉賺取流動性",
"subtitle": "參與活動獲得額外收益",
"duration": "活動時長",
"unlimited": "不限時",
"extraProfitRate": "額外利潤比例",
"notice": "此頁面僅供查看,鎖倉操作請透過 API 進行。",
"tiersTitle": "收益檔位",
"minAmount": "最小金額 (USDC)",
"tierLevel": "檔位 {n}",
"expectedProfit": "預期利潤",
"days": "{n} 天"
},
"locale": { "locale": {
"zh": "繁體中文", "zh": "繁體中文",
"en": "English", "en": "English",

View File

@ -10,6 +10,7 @@ import Search from '../views/Search.vue'
import Profile from '../views/Profile.vue' import Profile from '../views/Profile.vue'
import MemberCenter from '../views/MemberCenter.vue' import MemberCenter from '../views/MemberCenter.vue'
import ApiKey from '../views/ApiKey.vue' import ApiKey from '../views/ApiKey.vue'
import EarnActivity from '../views/EarnActivity.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -64,6 +65,11 @@ const router = createRouter({
name: 'api-key', name: 'api-key',
component: ApiKey, component: ApiKey,
}, },
{
path: '/earn-activity',
name: 'earn-activity',
component: EarnActivity,
},
], ],
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
const el = document.querySelector('[data-main-scroll]') const el = document.querySelector('[data-main-scroll]')

236
src/views/EarnActivity.vue Normal file
View File

@ -0,0 +1,236 @@
<template>
<div class="earn-activity-page">
<div class="earn-activity-screen">
<section class="card header-card">
<div class="header-icon">
<v-icon size="48" color="#5b5bd6">mdi-lock-outline</v-icon>
</div>
<h1 class="header-title">{{ t('earnActivity.title') }}</h1>
<p class="header-sub">{{ t('earnActivity.subtitle') }}</p>
</section>
<section class="card tiers-card" v-if="activityList.length > 0">
<h2 class="tiers-title">{{ t('earnActivity.tiersTitle') }}</h2>
<div class="tier-list">
<div class="tier-item" v-for="(item, index) in activityList" :key="item.ID">
<div class="tier-header">
<h3>{{ t('earnActivity.tierLevel', { n: index + 1 }) }}</h3>
</div>
<div class="tier-content">
<div class="info-row">
<span class="info-label">{{ t('earnActivity.minAmount') }}</span>
<span class="info-value">{{ item.minAmount ? item.minAmount.toLocaleString() : '--' }}</span>
</div>
<div class="info-divider"></div>
<div class="info-row">
<span class="info-label">{{ t('earnActivity.expectedProfit') }}</span>
<span class="info-value highlight">{{ formatRate(item.moreProfitRate) }}</span>
</div>
<div class="info-divider"></div>
<div class="info-row">
<span class="info-label">{{ t('earnActivity.duration') }}</span>
<span class="info-value">{{ formatDuration(item.seconds) }}</span>
</div>
</div>
</div>
</div>
</section>
<section class="card notice-card">
<div class="notice-icon">
<v-icon size="20" color="#ca8a04">mdi-information-outline</v-icon>
</div>
<p class="notice-text">{{ t('earnActivity.notice') }}</p>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { getPmEarnActivityPublic } from '@/api/earnActivity'
import type { PmEarnActivityItem } from '@/api/earnActivity'
const { t } = useI18n()
const activityList = ref<PmEarnActivityItem[]>([])
const loading = ref(false)
const formatRate = (rate: number | null) => {
if (!rate) return '--'
return `${(rate * 100).toFixed(0)}%`
}
const formatDuration = (seconds: number | null) => {
if (!seconds) return t('earnActivity.unlimited')
const days = Math.round(seconds / 86400)
return t('earnActivity.days', { n: days })
}
onMounted(async () => {
loading.value = true
try {
const res = await getPmEarnActivityPublic()
if (res.code === 0 && res.data) {
activityList.value = res.data
}
} catch (error) {
console.error('Failed to fetch earn activity list:', error)
} finally {
loading.value = false
}
})
</script>
<style scoped>
.earn-activity-page {
min-height: 100vh;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 16px;
}
.earn-activity-screen {
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 24px;
}
.header-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 12px;
}
.header-icon {
width: 80px;
height: 80px;
border-radius: 50%;
background: #f0f0ff;
display: flex;
align-items: center;
justify-content: center;
}
.header-title {
font-size: 24px;
font-weight: 700;
color: #111827;
margin: 0;
}
.header-sub {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.tiers-card {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
}
.tiers-title {
font-size: 18px;
font-weight: 600;
color: #111827;
margin: 0;
padding-bottom: 8px;
border-bottom: 1px solid #f3f4f6;
}
.tier-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.tier-item {
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.tier-header {
background: #f9fafb;
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
}
.tier-header h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #374151;
}
.tier-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.info-label {
font-size: 14px;
color: #6b7280;
}
.info-value {
font-size: 16px;
font-weight: 600;
color: #111827;
}
.info-value.highlight {
color: #5b5bd6;
}
.info-divider {
height: 1px;
background: #e5e7eb;
}
.notice-card {
display: flex;
align-items: flex-start;
gap: 12px;
background: #fffbeb;
border-color: #fde68a;
padding: 16px;
}
.notice-icon {
flex-shrink: 0;
margin-top: 2px;
}
.notice-text {
font-size: 13px;
color: #92400e;
margin: 0;
line-height: 1.5;
}
</style>