Explorar el Código

锁仓和团队页面

jean hace 7 horas
padre
commit
c6d14c0937

+ 4 - 0
.gitignore

@@ -16,3 +16,7 @@ deploy.mjs
 code/.env.local
 deploy-ibit.mjs
 AGENTS.md
+.idea/
+target/
+*.log
+*.class

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1554 - 436
code/pnpm-lock.yaml


+ 36 - 0
code/src/api/staking.ts

@@ -71,6 +71,42 @@ export interface StakingOrderItem {
   createTime?: string
 }
 
+/** IDO 页「我的团队」人数:优先经纪商 info,其次邀请列表长度 */
+export async function fetchIdoTeamCount(): Promise<number> {
+  const pickCount = (raw: unknown): number | null => {
+    if (!raw || typeof raw !== 'object') return null
+    const r = raw as Record<string, unknown>
+    const keys = [
+      'teamCount',
+      'teamMemberCount',
+      'memberCount',
+      'childCount',
+      'totalMember',
+      'inviteCount',
+      'promotionCount',
+      'totalInvited',
+    ]
+    for (const k of keys) {
+      const n = Number(r[k])
+      if (Number.isFinite(n) && n >= 0) return Math.floor(n)
+    }
+    return null
+  }
+  try {
+    const fromInfo = pickCount(await post<unknown>('/uc/agent/info', {}))
+    if (fromInfo != null) return fromInfo
+  } catch {
+    // 非经纪商或接口不可用
+  }
+  try {
+    const list = await post<unknown>('/uc/agent/reward/set/list', {})
+    if (Array.isArray(list)) return list.length
+  } catch {
+    // 静默
+  }
+  return 0
+}
+
 /** 分页查询我的质押订单,status 支持单个状态或逗号分隔状态,如 "0,1" */
 export function fetchStakingOrders(pageNo = 1, pageSize = 20, status?: string | number) {
   return postForm<SpringPage<StakingOrderItem>>('/uc/staking/orders', {

+ 7 - 0
code/src/locales/financePage.i18n.ts

@@ -7,6 +7,7 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     idoRuleLabel: '规则:',
     idoRuleOnce: '会员购买一定量平台自己的主链币,自动进入锁仓账户。锁仓{lock}个月,到期一次性释放。',
     idoRuleBatch: '会员购买一定量平台自己的主链币,自动进入锁仓账户。锁仓{lock}个月,按{release}个月释放。',
+    myTeam: '我的团队',
     joinPresale: '参与预售',
     subscribeQty: '认购数量',
     availableIbit: '可提现可用 iBit',
@@ -71,6 +72,7 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     idoRuleLabel: '規則:',
     idoRuleOnce: '會員購買一定量平台自己的主鏈幣,自動進入鎖倉帳戶。鎖倉{lock}個月,到期一次性釋放。',
     idoRuleBatch: '會員購買一定量平台自己的主鏈幣,自動進入鎖倉帳戶。鎖倉{lock}個月,按{release}個月釋放。',
+    myTeam: '我的團隊',
     joinPresale: '參與預售',
     subscribeQty: '認購數量',
     availableIbit: '可提现可用 iBit',
@@ -134,6 +136,7 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     idoRuleLabel: 'Rules: ',
     idoRuleOnce: 'Members purchase a set amount of the platform main-chain token; funds are locked automatically. Locked for {lock} month(s), then released in full.',
     idoRuleBatch: 'Members purchase a set amount of the platform main-chain token; funds are locked automatically. Locked for {lock} month(s), released over {release} month(s).',
+    myTeam: 'My Team',
     joinPresale: 'Join Presale',
     subscribeQty: 'Subscription Amount',
     availableIbit: 'Withdrawable iBit',
@@ -198,6 +201,7 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     idoRuleLabel: '규칙: ',
     idoRuleOnce: '회원이 일정량의 플랫폼 메인체인 코인을 구매하면 자동으로 락업 계정에 들어갑니다. {lock}개월 락업 후 일괄 해제됩니다.',
     idoRuleBatch: '회원이 일정량의 플랫폼 메인체인 코인을 구매하면 자동으로 락업 계정에 들어갑니다. {lock}개월 락업 후 {release}개월에 걸쳐 해제됩니다.',
+    myTeam: '내 팀',
     joinPresale: '프리세일 참여',
     subscribeQty: '청약 수량',
     availableIbit: '출금 가능 iBit',
@@ -261,6 +265,7 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     idoRuleLabel: 'ルール:',
     idoRuleOnce: '会員が一定量のプラットフォーム主チェーンコインを購入すると、自動的にロック口座に入ります。{lock}ヶ月ロック後、一括解放されます。',
     idoRuleBatch: '会員が一定量のプラットフォーム主チェーンコインを購入すると、自動的にロック口座に入ります。{lock}ヶ月ロック後、{release}ヶ月かけて解放されます。',
+    myTeam: 'マイチーム',
     joinPresale: 'プレセール参加',
     subscribeQty: '申込数量',
     availableIbit: '出金可能 iBit',
@@ -324,6 +329,7 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     idoRuleLabel: 'नियम: ',
     idoRuleOnce: 'सदस्य प्लेटफ़ॉर्म के मुख्य चेन सिक्के की निश्चित मात्रा खरीदते हैं, राशि स्वतः लॉक खाते में जाती है। {lock} महीने लॉक, फिर एकमुश्त रिलीज़।',
     idoRuleBatch: 'सदस्य प्लेटफ़ॉर्म के मुख्य चेन सिक्के की निश्चित मात्रा खरीदते हैं, राशि स्वतः लॉक खाते में जाती है। {lock} महीने लॉक, {release} महीनों में रिलीज़।',
+    myTeam: 'मेरी टीम',
     joinPresale: 'प्रीसेल में शामिल हों',
     subscribeQty: 'सदस्यता राशि',
     availableIbit: 'निकासी योग्य iBit',
@@ -387,6 +393,7 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     idoRuleLabel: 'Aturan: ',
     idoRuleOnce: 'Anggota membeli token main-chain platform dalam jumlah tertentu dan otomatis masuk akun lock. Terkunci {lock} bulan, lalu dilepas sekaligus.',
     idoRuleBatch: 'Anggota membeli token main-chain platform dalam jumlah tertentu dan otomatis masuk akun lock. Terkunci {lock} bulan, dilepas selama {release} bulan.',
+    myTeam: 'Tim Saya',
     joinPresale: 'Ikut Presale',
     subscribeQty: 'Jumlah Langganan',
     availableIbit: 'iBit dapat ditarik',

+ 16 - 1
code/src/views/assets/AssetsView.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { ref, computed, watch, onMounted, onActivated } from 'vue'
 import { useI18n } from 'vue-i18n'
-import { useRouter } from 'vue-router'
+import { useRoute, useRouter } from 'vue-router'
 import { get, post, postForm } from '@/utils/request'
 import { unwrapSpotAssetPayload, extractUsdtAvailableFromSpotAssets, involvesSpotTradingBridge } from '@/utils/spotAsset'
 import {
@@ -21,6 +21,7 @@ import { storeToRefs } from 'pinia'
 import { fetchStakingOrders, type StakingOrderItem } from '@/api/staking'
 
 const { t } = useI18n()
+const route = useRoute()
 const router = useRouter()
 const toast = useToast()
 const coinsStore = useCoinsStore()
@@ -599,6 +600,18 @@ function endStakingPull() {
   stakingPullDistance.value = 0
 }
 
+function applyTabFromRoute() {
+  const raw = route.query.tab
+  const v = Array.isArray(raw) ? raw[0] : raw
+  if (v === 'staking' || v === 'locked') {
+    activeTab.value = ASSET_TABS.STAKING
+  }
+}
+
+watch(() => route.query.tab, () => {
+  applyTabFromRoute()
+})
+
 watch(activeTab, (tabIdx) => {
   void loadAssets({ silent: true })
   if (tabIdx === ASSET_TABS.FUND) {
@@ -613,6 +626,7 @@ watch(activeTab, (tabIdx) => {
 })
 
 onMounted(() => {
+  applyTabFromRoute()
   if (!coinsStore.fetched) {
     coinsStore.fetchCoins()
   }
@@ -620,6 +634,7 @@ onMounted(() => {
   loadAssets()
 })
 onActivated(() => {
+  applyTabFromRoute()
   loadAssets()
   if (activeTab.value === ASSET_TABS.FOLLOW) {
     void refreshFollowCopyPositions()

+ 111 - 3
code/src/views/finance/StakeView.vue

@@ -3,12 +3,13 @@ import { computed, onMounted, ref, watch } from 'vue'
 import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import {
+  fetchIdoTeamCount,
   fetchMinOpenStakingConfig,
   fetchStakingConfigList,
   submitStake,
   type StakingConfig,
 } from '@/api/staking'
-import { loadFundCoinBalance, STAKING_COIN_UNIT } from '@/utils/stakingWallet'
+import { loadFundCoinBalance, loadStakingWalletSnapshot, STAKING_COIN_UNIT } from '@/utils/stakingWallet'
 import {
   estimatedUnlockDate,
   formatUnlockDate,
@@ -33,7 +34,8 @@ const subscribeQty = ref('')
 const error = ref<string | null>(null)
 const availableBal = ref('0')
 const showTransfer = ref(false)
-
+const teamCount = ref(0)
+const lockedTotal = ref('0')
 const configIdParam = computed(() => {
   const raw = route.params.configId
   if (raw === undefined || raw === '' || raw === 'ido') return null
@@ -141,6 +143,31 @@ async function refreshWithdrawableIbit() {
   }
 }
 
+async function refreshOverviewStats() {
+  const unit = stakingCoinUnit()
+  const [stakingSnap, team] = await Promise.all([
+    loadStakingWalletSnapshot(unit).catch(() => ({
+      available: '0',
+      locked: '0',
+      availableUsdt: '0',
+      lockedUsdt: '0',
+    })),
+    fetchIdoTeamCount().catch(() => 0),
+  ])
+  const total =
+    Number(stakingSnap.available) + Number(stakingSnap.locked)
+  lockedTotal.value = Number.isFinite(total) ? String(total) : '0'
+  teamCount.value = team
+}
+
+function goToBroker() {
+  router.push({ name: 'broker' })
+}
+
+function goToStakingAccount() {
+  router.push({ name: 'assets', query: { tab: 'staking' } })
+}
+
 async function onTransferSuccess() {
   showTransfer.value = false
   await load({ silent: true })
@@ -172,7 +199,7 @@ async function load(opts?: { silent?: boolean }) {
       error.value = t('finance.configNotFound')
       return
     }
-    await refreshWithdrawableIbit()
+    await Promise.all([refreshWithdrawableIbit(), refreshOverviewStats()])
   } catch (e) {
     error.value = e instanceof Error ? e.message : t('common.loadFailed')
     config.value = null
@@ -213,6 +240,7 @@ async function onSubmit() {
     toast.success(t('finance.stakeSuccess'))
     subscribeQty.value = ''
     await load({ silent: true })
+    await refreshOverviewStats()
   } catch (e) {
     toast.error(e instanceof Error ? e.message : t('finance.stakeFailed'))
   } finally {
@@ -253,6 +281,26 @@ watch(() => route.params.configId, () => load())
         </div>
       </section>
 
+      <!-- 团队 / 锁仓总览 -->
+      <section class="ido-overview-section page-container">
+        <div class="ido-overview-grid">
+          <button type="button" class="ido-overview-card" @click="goToBroker">
+            <span class="ido-overview-label">
+              {{ t('finance.myTeam') }}
+              <span class="ido-overview-chevron" aria-hidden="true">&gt;</span>
+            </span>
+            <span class="ido-overview-value num">{{ fmtAmount(teamCount, 0) }}</span>
+          </button>
+          <button type="button" class="ido-overview-card" @click="goToStakingAccount">
+            <span class="ido-overview-label">
+              {{ t('assets.stakingTotal') }}
+              <span class="ido-overview-chevron" aria-hidden="true">&gt;</span>
+            </span>
+            <span class="ido-overview-value num">{{ fmtAmount(lockedTotal, 2) }}</span>
+          </button>
+        </div>
+      </section>
+
       <!-- 参与预售 -->
       <section class="ido-form-section page-container">
         <h2 class="section-title">{{ t('finance.joinPresale') }}</h2>
@@ -461,6 +509,58 @@ watch(() => route.params.configId, () => load())
   display: block;
 }
 
+/* Overview */
+.ido-overview-section {
+  max-width: 960px;
+  padding-bottom: var(--space-6);
+}
+
+.ido-overview-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: var(--space-4);
+}
+
+.ido-overview-card {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: var(--space-4);
+  padding: var(--space-5) var(--space-6);
+  background: rgba(255, 255, 255, 0.04);
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  border-radius: 16px;
+  cursor: pointer;
+  text-align: left;
+  transition: border-color 0.2s, background 0.2s;
+}
+
+.ido-overview-card:hover {
+  border-color: rgba(240, 185, 11, 0.35);
+  background: rgba(240, 185, 11, 0.06);
+}
+
+.ido-overview-label {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.55);
+}
+
+.ido-overview-chevron {
+  color: rgba(255, 255, 255, 0.35);
+  font-size: 13px;
+}
+
+.ido-overview-value {
+  font-size: clamp(28px, 4vw, 36px);
+  font-weight: 600;
+  color: #fff;
+  line-height: 1.1;
+  font-variant-numeric: tabular-nums;
+}
+
 /* Form */
 .ido-form-section {
   max-width: 960px;
@@ -695,6 +795,14 @@ watch(() => route.params.configId, () => load())
 }
 
 @media (max-width: 640px) {
+  .ido-overview-grid {
+    gap: var(--space-3);
+  }
+
+  .ido-overview-card {
+    padding: var(--space-4) var(--space-5);
+  }
+
   .subscribe-grid {
     grid-template-columns: 1fr;
   }

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio