|
|
@@ -1,82 +1,448 @@
|
|
|
+<script setup>
|
|
|
+import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
|
|
+
|
|
|
+// --- 1. 状态管理 ---
|
|
|
+const tabs = [
|
|
|
+ { key: 'fav', label: '自选' },
|
|
|
+ { key: 'spot', label: '币币' },
|
|
|
+ { key: 'futures', label: '合约' },
|
|
|
+ { key: 'seconds', label: '秒合约' }
|
|
|
+];
|
|
|
+
|
|
|
+const currentTab = ref('spot');
|
|
|
+const listData = ref([]);
|
|
|
+const page = ref(1);
|
|
|
+const loading = ref(false);
|
|
|
+const finished = ref(false);
|
|
|
+const scrollContainer = ref(null);
|
|
|
+const PAGE_SIZE = 15; // 定义分页大小
|
|
|
+
|
|
|
+// --- 2. 图表与图标工具函数 (保持不变) ---
|
|
|
+const generateChartPaths = (isUp) => {
|
|
|
+ const width = 60; const height = 24; const pointCount = 15; const step = width / (pointCount - 1);
|
|
|
+ let points = []; let y = isUp ? (height * 0.8) : (height * 0.2);
|
|
|
+ for(let i=0; i<pointCount; i++) {
|
|
|
+ const x = i * step;
|
|
|
+ y += (Math.random() - 0.5) * (height * 0.4) + (isUp ? -(height * 0.05) : (height * 0.05));
|
|
|
+ y = Math.max(2, Math.min(height - 2, y));
|
|
|
+ points.push({x, y});
|
|
|
+ }
|
|
|
+ const linePath = `M ${points.map(p => `${p.x},${p.y}`).join(' L ')}`;
|
|
|
+ const fillPath = `${linePath} L ${width},${height + 5} L 0,${height + 5} Z`;
|
|
|
+ return { linePath, fillPath };
|
|
|
+};
|
|
|
+
|
|
|
+const icons = {
|
|
|
+ BTC: '<path fill="#F7931A" d="M22.6 13.4c.4-2.6-1.6-4-4.3-4.9l.9-3.5-2.1-.5-.9 3.5c-.5-.1-1.1-.3-1.7-.4l.9-3.5-2.1-.5-.9 3.5c-.4-.1-1-.2-1.5-.3l-3-.7-.6 2.4s1.6.4 1.6.4c.9.2 1 .8 1 1.2l-1 4c.1 0 .1 0 .1.1l-1.4 5.6c0 .3-.3.8-1 .6 0 0-1.6-.4-1.6-.4l-1.1 2.6 2.8.7c.5.1 1 .3 1.5.4l-.9 3.6 2.1.5.9-3.6c.6.2 1.1.3 1.7.5l-.9 3.6 2.1.5.9-3.5c3.7.7 6.4.4 7.6-2.9.9-2.6-.1-4.1-1.9-5.1 1.4-.3 2.4-1.2 2.7-3z"/>',
|
|
|
+ ETH: '<path fill="#627EEA" d="M11.9 20.3L6.1 16.9l5.8-2.6 5.8 2.6-5.8 3.4zm0-9.6l5.8 2.6-5.8 8.1-5.8-8.1 5.8-2.6zM12 2l5.8 9.6L12 14 6.2 11.6 12 2z"/>',
|
|
|
+ BNB: '<path fill="#F3BA2F" d="M4.6 12l2.3-2.3L9.2 12l-2.3 2.3L4.6 12zM12 4.6l2.3 2.3-2.3 2.3-2.3-2.3L12 4.6zm7.4 7.4l-2.3 2.3 2.3 2.3 2.3-2.3-2.3-2.3zm-7.4 7.4l-2.3-2.3 2.3-2.3 2.3 2.3-2.3 2.3zm4.6-7.4l2.3-2.3-2.3-2.3-2.3 2.3 2.3 2.3zM12 14.8l-2.3-2.3 2.3-2.3 2.3 2.3L12 14.8zM9.7 7.4L12 5.1l2.3 2.3L12 9.7 9.7 7.4z"/>',
|
|
|
+ USDT: '<path fill="#26A17B" d="M14.5 10.4c.2-.2.3-.3.3-.3s-.1 0-.3 0c-.5.1-1.2.1-1.9.1-.1-2.9 0-2.9 0-2.9h3.1V5.7h-3.1V3H11v2.7H8V7.3h3v2.9c0 .1 0 .2-.1.2-2.8 0-5.1.8-5.1 1.7 0 1 2.3 1.7 5.2 1.7s5.2-.8 5.2-1.7c0-.7-1.3-1.2-3.4-1.5z"/>'
|
|
|
+};
|
|
|
+
|
|
|
+// --- 3. 模拟 WebSocket 服务 ---
|
|
|
+const mockSocket = {
|
|
|
+ activeSubs: new Set(),
|
|
|
+ interval: null,
|
|
|
+ subscribe(ids, callback) {
|
|
|
+ ids.forEach(id => this.activeSubs.add(id));
|
|
|
+ if (!this.interval) {
|
|
|
+ this.interval = setInterval(() => {
|
|
|
+ const subArray = Array.from(this.activeSubs);
|
|
|
+ if (subArray.length === 0) return;
|
|
|
+ const updates = [];
|
|
|
+ const updateCount = Math.floor(Math.random() * 5) + 1;
|
|
|
+ for(let i=0; i<updateCount; i++) {
|
|
|
+ const randomId = subArray[Math.floor(Math.random() * subArray.length)];
|
|
|
+ const isUp = Math.random() > 0.5;
|
|
|
+ updates.push({
|
|
|
+ id: randomId,
|
|
|
+ price: 40000 + Math.random() * 1000,
|
|
|
+ change: (isUp ? 1 : -1) * (Math.random() * 5).toFixed(2),
|
|
|
+ isUp: isUp
|
|
|
+ });
|
|
|
+ }
|
|
|
+ callback(updates);
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ unsubscribeAll() {
|
|
|
+ this.activeSubs.clear();
|
|
|
+ if (this.interval) {
|
|
|
+ clearInterval(this.interval);
|
|
|
+ this.interval = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// --- 4. 模拟 REST API ---
|
|
|
+const mockFetchData = (tab, pageNum) => {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ setTimeout(() => {
|
|
|
+ // 为了测试效果,改为只返回 10 条数据(小于 PAGE_SIZE 15)
|
|
|
+ // 这样第一页加载完就会立刻显示“没有更多了”
|
|
|
+ const count = 10;
|
|
|
+
|
|
|
+ // 如果不是第一页,就直接返回空,确保结束
|
|
|
+ if (pageNum > 1) { resolve([]); return; }
|
|
|
+
|
|
|
+ const newItems = Array.from({ length: count }).map((_, i) => {
|
|
|
+ const id = `${tab}-${(pageNum - 1) * PAGE_SIZE + i + 1}`;
|
|
|
+ const isUp = Math.random() > 0.5;
|
|
|
+ const paths = generateChartPaths(isUp);
|
|
|
+
|
|
|
+ let symbolPrefix = 'ABC';
|
|
|
+ if (tab === 'fav') symbolPrefix = 'FAV';
|
|
|
+ if (tab === 'spot') symbolPrefix = 'BTC';
|
|
|
+ if (tab === 'futures') symbolPrefix = 'ETH';
|
|
|
+
|
|
|
+ let realSymbol = symbolPrefix + ((pageNum - 1) * PAGE_SIZE + i + 1);
|
|
|
+ if (i % 5 === 0) realSymbol = 'BTC';
|
|
|
+ if (i % 5 === 1) realSymbol = 'ETH';
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: id,
|
|
|
+ symbol: realSymbol,
|
|
|
+ name: `${realSymbol} Network`,
|
|
|
+ price: 40000 + Math.random() * 5000,
|
|
|
+ cny: 280000 + Math.random() * 30000,
|
|
|
+ change: (isUp ? 1 : -1) * (Math.random() * 10).toFixed(2),
|
|
|
+ svgIcon: icons[realSymbol],
|
|
|
+ chartLine: paths.linePath,
|
|
|
+ chartFill: paths.fillPath,
|
|
|
+ chartColor: isUp ? '#2EBD85' : '#F6465D',
|
|
|
+ btnClass: isUp ? 'btn-green' : 'btn-red'
|
|
|
+ };
|
|
|
+ });
|
|
|
+ resolve(newItems);
|
|
|
+ }, 500);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// --- 5. 核心逻辑:数据加载与订阅 ---
|
|
|
+
|
|
|
+const handleSocketUpdate = (updates) => {
|
|
|
+ updates.forEach(update => {
|
|
|
+ const item = listData.value.find(i => i.id === update.id);
|
|
|
+ if (item) {
|
|
|
+ item.price = update.price;
|
|
|
+ item.change = update.change;
|
|
|
+ item.cny = update.price * 7.2;
|
|
|
+ const isUp = update.change >= 0;
|
|
|
+ item.btnClass = isUp ? 'btn-green' : 'btn-red';
|
|
|
+ item.chartColor = isUp ? '#2EBD85' : '#F6465D';
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const onLoad = async () => {
|
|
|
+ if (loading.value || finished.value) return;
|
|
|
+
|
|
|
+ loading.value = true;
|
|
|
+ const data = await mockFetchData(currentTab.value, page.value);
|
|
|
+
|
|
|
+ // 核心优化逻辑:
|
|
|
+ if (data.length === 0) {
|
|
|
+ finished.value = true;
|
|
|
+ } else {
|
|
|
+ listData.value.push(...data);
|
|
|
+ page.value++;
|
|
|
+
|
|
|
+ // 如果返回的数据条数小于分页大小,说明已经是最后一页了
|
|
|
+ // 直接标记完成,这样不需要再发一次空请求
|
|
|
+ if (data.length < PAGE_SIZE) {
|
|
|
+ finished.value = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ const newIds = data.map(item => item.id);
|
|
|
+ mockSocket.subscribe(newIds, handleSocketUpdate);
|
|
|
+ }
|
|
|
+ loading.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 监听 Tab 切换
|
|
|
+watch(currentTab, () => {
|
|
|
+ mockSocket.unsubscribeAll();
|
|
|
+ listData.value = [];
|
|
|
+ page.value = 1;
|
|
|
+ finished.value = false;
|
|
|
+ loading.value = false;
|
|
|
+ if (scrollContainer.value) scrollContainer.value.scrollTop = 0;
|
|
|
+ onLoad();
|
|
|
+});
|
|
|
+
|
|
|
+const handleScroll = (e) => {
|
|
|
+ const { scrollTop, clientHeight, scrollHeight } = e.target;
|
|
|
+ if (scrollTop + clientHeight >= scrollHeight - 50) {
|
|
|
+ onLoad();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => onLoad());
|
|
|
+onUnmounted(() => mockSocket.unsubscribeAll());
|
|
|
+
|
|
|
+const formatPrice = (val) => val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
|
+const formatCNY = (val) => val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
|
+</script>
|
|
|
+
|
|
|
<template>
|
|
|
- <div class="market">
|
|
|
- <div class="market-nav">
|
|
|
- <div class="nav-left">
|
|
|
- <div class="pf600 fs18 fc121212" @click="messageChange('selfSelected')">自选</div>
|
|
|
- <div class="sys-notifi pf600 fs14 fcA8A8A8" @click="messageChange('bibi')">
|
|
|
- 币币
|
|
|
+ <div class="market-page">
|
|
|
+ <svg width="0" height="0" style="position:absolute;">
|
|
|
+ <defs>
|
|
|
+ <linearGradient id="chart-grad-up" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
|
+ <stop offset="0%" style="stop-color:#2EBD85;stop-opacity:0.2" />
|
|
|
+ <stop offset="100%" style="stop-color:#2EBD85;stop-opacity:0" />
|
|
|
+ </linearGradient>
|
|
|
+ <linearGradient id="chart-grad-down" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
|
+ <stop offset="0%" style="stop-color:#F6465D;stop-opacity:0.2" />
|
|
|
+ <stop offset="100%" style="stop-color:#F6465D;stop-opacity:0" />
|
|
|
+ </linearGradient>
|
|
|
+ </defs>
|
|
|
+ </svg>
|
|
|
+
|
|
|
+ <div class="sticky-header">
|
|
|
+ <div class="top-bar">
|
|
|
+ <div class="tabs-wrapper">
|
|
|
+ <div
|
|
|
+ v-for="tab in tabs"
|
|
|
+ :key="tab.key"
|
|
|
+ @click="currentTab = tab.key"
|
|
|
+ class="tab-item"
|
|
|
+ :class="{ active: currentTab === tab.key }"
|
|
|
+ >
|
|
|
+ {{ tab.label }}
|
|
|
+ <div v-if="currentTab === tab.key" class="active-indicator"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="search-icon">
|
|
|
+ <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#707A8A" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="col col-left">交易对</div>
|
|
|
+ <div class="col col-center">最新价</div>
|
|
|
+ <div class="col col-right">今日涨跌幅</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ class="list-container"
|
|
|
+ ref="scrollContainer"
|
|
|
+ @scroll="handleScroll"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-for="coin in listData"
|
|
|
+ :key="coin.id"
|
|
|
+ class="list-item"
|
|
|
+ >
|
|
|
+ <div class="col col-left coin-info">
|
|
|
+ <div class="coin-icon-wrapper">
|
|
|
+ <svg v-if="coin.svgIcon" viewBox="0 0 32 32" class="real-icon" v-html="coin.svgIcon"></svg>
|
|
|
+ <div v-else class="placeholder-icon">{{ coin.symbol[0] }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="text-group">
|
|
|
+ <div class="name-row">{{ coin.name }}</div>
|
|
|
+ <div class="symbol-row">{{ coin.symbol }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="col col-center chart-box">
|
|
|
+ <svg width="60" height="24" viewBox="0 0 60 24">
|
|
|
+ <path :d="coin.chartFill" :fill="coin.change >= 0 ? 'url(#chart-grad-up)' : 'url(#chart-grad-down)'" />
|
|
|
+ <path :d="coin.chartLine" fill="none" :stroke="coin.chartColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
</div>
|
|
|
- <div class="sys-notifi pf600 fs14 fcA8A8A8" @click="messageChange('contract')">
|
|
|
- 合约
|
|
|
+
|
|
|
+ <div class="col col-right price-info">
|
|
|
+ <div class="price transition-colors duration-300" :class="coin.change >= 0 ? 'text-[#2EBD85]' : 'text-[#F6465D]'">
|
|
|
+ {{ formatPrice(coin.price) }}
|
|
|
+ </div>
|
|
|
+ <div class="cny">¥ {{ formatCNY(coin.cny) }}</div>
|
|
|
</div>
|
|
|
- <div
|
|
|
- class="sys-notifi pf600 fs14 fcA8A8A8"
|
|
|
- @click="messageChange('secondContract')">
|
|
|
- 秒合约
|
|
|
+ <div>
|
|
|
+ <button class="change-btn transition-colors duration-300" :class="coin.btnClass">
|
|
|
+ {{ coin.change >= 0 ? '+' : '' }}{{ coin.change }}%
|
|
|
+ </button>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="nav-right">
|
|
|
- <img src="../../assets/icon/market/search.svg" alt="" />
|
|
|
+
|
|
|
+ <!-- 底部状态区:使用 v-show 或 v-if 确保渲染 -->
|
|
|
+ <div class="loading-state">
|
|
|
+ <div v-if="loading" class="spinner-container">
|
|
|
+ <div class="spinner"></div>
|
|
|
+ <span class="loading-text">加载中...</span>
|
|
|
+ </div>
|
|
|
+ <!-- 这里确保 finished 为 true 时一定会显示 -->
|
|
|
+ <div v-else-if="finished" class="no-more-text">
|
|
|
+ - 没有更多了 -
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <component :is="currentComponent" />
|
|
|
</div>
|
|
|
</template>
|
|
|
-<script setup>
|
|
|
- import Contract from "./Contract.vue";
|
|
|
- import SecondContract from "./SecondContract.vue";
|
|
|
- import SelfSelected from "./SelfSelected.vue";
|
|
|
- import Bibi from "./Bibi.vue";
|
|
|
- import { ref, computed } from "vue";
|
|
|
-
|
|
|
- const current = ref("selfSelected");
|
|
|
- const componentsMap = {
|
|
|
- contract: Contract,
|
|
|
- bibi: Bibi,
|
|
|
- secondContract: SecondContract,
|
|
|
- selfSelected: SelfSelected,
|
|
|
- };
|
|
|
- const currentComponent = computed(() => componentsMap[current.value]);
|
|
|
-
|
|
|
- const messageChange = (key) => {
|
|
|
- current.value = key;
|
|
|
- };
|
|
|
-</script>
|
|
|
-<style lang="less" scoped>
|
|
|
- .market {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- justify-content: flex-start;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 100px;
|
|
|
- width: 100%;
|
|
|
-
|
|
|
- .market-nav {
|
|
|
- display: flex;
|
|
|
- flex-direction: row;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- margin-top: 21px;
|
|
|
- width: 345px;
|
|
|
- height: 24px;
|
|
|
-
|
|
|
- .nav-left {
|
|
|
- display: flex;
|
|
|
- flex-direction: row;
|
|
|
- justify-content: flex-start;
|
|
|
- align-items: flex-end;
|
|
|
- width: 345px;
|
|
|
- height: 24px;
|
|
|
-
|
|
|
- .sys-notifi {
|
|
|
- margin-left: 35px;
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- .nav-right {
|
|
|
- width: 20px;
|
|
|
- height: 20px;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-</style>
|
|
|
+<style scoped>
|
|
|
+/* 基础重置 */
|
|
|
+* { box-sizing: border-box; }
|
|
|
+
|
|
|
+.market-page {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ background-color: #fff;
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
|
+ color: #1E2329;
|
|
|
+}
|
|
|
+
|
|
|
+.sticky-header {
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ background-color: #fff;
|
|
|
+ z-index: 10;
|
|
|
+ padding: 12px 16px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.top-bar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.tabs-wrapper {
|
|
|
+ display: flex;
|
|
|
+ gap: 28px;
|
|
|
+ align-items: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-item {
|
|
|
+ position: relative;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #707A8A;
|
|
|
+ font-weight: 500;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ line-height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-item.active {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1E2329;
|
|
|
+}
|
|
|
+
|
|
|
+.active-indicator {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 16px;
|
|
|
+ height: 3px;
|
|
|
+ background-color: #1E2329;
|
|
|
+ border-radius: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.table-header {
|
|
|
+ display: flex;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #707A8A;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.col { display: flex; }
|
|
|
+.col-left { width: 38%; justify-content: flex-start; align-items: center; }
|
|
|
+.col-center { width: 25%; justify-content: center; align-items: center; margin-left: 10px; }
|
|
|
+.col-right { width: 37%; justify-content: flex-end; align-items: center; }
|
|
|
+
|
|
|
+.list-container {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ /* 增加底部 Padding,确保“没有更多了”不被遮挡 */
|
|
|
+ padding: 0 15px 60px;
|
|
|
+}
|
|
|
+
|
|
|
+.list-item {
|
|
|
+ display: flex;
|
|
|
+ padding: 16px 0;
|
|
|
+ border-bottom: 1px solid #F0F3F5;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.coin-icon-wrapper {
|
|
|
+ width: 28px; height: 28px; margin-right: 8px; flex-shrink: 0;
|
|
|
+}
|
|
|
+.real-icon { width: 100%; height: 100%; }
|
|
|
+.placeholder-icon {
|
|
|
+ width: 100%; height: 100%; background: #F0F3F5; border-radius: 50%;
|
|
|
+ color: #707A8A; display: flex; align-items: center; justify-content: center;
|
|
|
+ font-size: 12px; font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.text-group { display: flex; flex-direction: column; }
|
|
|
+.name-row { font-size: 14px; font-weight: 600; color: #1E2329; margin-bottom: 2px; }
|
|
|
+.symbol-row { font-size: 12px; color: #707A8A; }
|
|
|
+
|
|
|
+.chart-box svg { overflow: visible; }
|
|
|
+
|
|
|
+.price-info.col-right {
|
|
|
+ display: inline-block; text-align: right; margin-right: 8px;
|
|
|
+}
|
|
|
+.price { font-size: 14px; font-weight: 600; }
|
|
|
+.cny { font-size: 11px; color: #707A8A; margin-bottom: 4px; transform: scale(0.95); transform-origin: right center; }
|
|
|
+
|
|
|
+.change-btn {
|
|
|
+ width: 72px; height: 32px; border: none; border-radius: 4px;
|
|
|
+ color: #fff; font-size: 13px; font-weight: 500;
|
|
|
+ display: flex; align-items: center; justify-content: center; margin-top: 2px;
|
|
|
+}
|
|
|
+.btn-green { background-color: #2EBD85; }
|
|
|
+.btn-red { background-color: #F6465D; }
|
|
|
+
|
|
|
+.loading-state {
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px 0;
|
|
|
+ color: #999;
|
|
|
+ font-size: 12px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 80px; /* 增加高度确保可见 */
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.spinner-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.spinner {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ border: 2px solid #e0e0e0;
|
|
|
+ border-top-color: #707A8A;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 0.8s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化后的“没有更多了”样式:深灰色文字 */
|
|
|
+.no-more-text {
|
|
|
+ color: #999;
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 8px 16px;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ from { transform: rotate(0deg); }
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+.text-[#2EBD85] { color: #2EBD85; }
|
|
|
+.text-[#F6465D] { color: #F6465D; }
|
|
|
+</style>
|