|
|
<template> <div class="top"> <el-card style="width:20%" class="center-card">数据总览</el-card> <span class="text"> 最后更新时间:{{ workDataUpdateTime && workDataUpdateTime !== '1970-01-01 08:00:00' ? workDataUpdateTime : '该地区暂无数据' }} </span> </div>
<div class="card"> <!-- 第一个卡片 --> <el-card class="card-item"> <template #header> <div class="card-title">当前金币余量</div> <div> <span style="font-weight: bold">{{ currentGold / 100 }}</span> 较前一日 {{ dailyChange / 100 }} <template v-if="dailyChange > 0"> <el-icon style="color:red"> <ArrowUpBold /> </el-icon> </template> <template v-else-if="dailyChange < 0"> <el-icon style="color:forestgreen"> <ArrowDownBold /> </el-icon> </template> <template v-else> <el-icon style="color:grey"> <SemiSelect /> </el-icon> </template> </div> </template> <div> <div class="margin-bottom">永久金币:{{ currentPermanent / 100 }}</div> <div class="margin-bottom">免费金币:{{ currentFree / 100 }}</div> <div class="margin-bottom">[六月到期|{{ currentFreeJune / 100 }}] [十二月到期|{{ currentFreeDecember / 100 }}] </div> <div>任务金币:{{ currentTask / 100 }}</div> </div> </el-card>
<!-- 第二个卡片 --> <el-card class="card-item"> <div class="card-title">全年累计充值金币数</div> <div class="card-title">{{ yearlyRecharge / 100 }}</div> <div> </div> <div class="center-card">折合新币累计金额:{{ yearlyMoney / 100 }}</div> <template #footer> <el-col class="margin-bottom center-card">昨日新增金币:{{ recharge / 100 }}</el-col> <el-col class="margin-bottom center-card">其中永久金币:{{ money / 100 }}</el-col> </template> </el-card>
<!-- 第三个卡片 --> <el-card class="card-item"> <div class="card-title">全年累计消费金币数</div> <div class="card-title">{{ yearlyReduce / 100 }}</div> <div style="padding-left: 30%;">消耗:{{ yearlyConsume / 100 }}</div> <div style="padding-left: 30%;">退款:{{ yearlyRefund / 100 }}</div> <template #footer> <div style="margin-bottom:0.5%;padding-left: 30%;">昨日新增消费:{{ dailyConsume / 100 }}</div> <div style="margin-bottom:0.5%;padding-left: 30%;">昨日新增消耗:{{ dailyReduce / 100 }}</div> <div style="margin-bottom:0.5%;padding-left: 30%;">昨日新增退款:{{ dailyRefund / 100 }}</div> </template> </el-card>
<!-- 第四个卡片 --> <el-card class="card-item"> <el-col class="card-title">全年累计充值人头数</el-col> <el-col class="card-title">{{ yearlyRechargeNum }}</el-col> <el-col style="padding-left: 35%;">周同比:{{ sumWow }}% <template v-if="sumWow > 0"> <el-icon style="color:red"> <ArrowUpBold /> </el-icon> </template> <template v-else-if="sumWow < 0"> <el-icon style="color:forestgreen"> <ArrowDownBold /> </el-icon> </template> <template v-else> <el-icon style="color:grey"> <SemiSelect /> </el-icon> </template> </el-col> <el-col style="padding-left: 35%;">日环比:{{ sumDaily }}% <template v-if="sumDaily > 0"> <el-icon style="color:red"> <ArrowUpBold /> </el-icon> </template> <template v-else-if="sumDaily < 0"> <el-icon style="color:forestgreen"> <ArrowDownBold /> </el-icon> </template> <template v-else> <el-icon style="color:grey"> <SemiSelect /> </el-icon> </template> </el-col> <template #footer> <el-col style="padding-left: 35%;margin-bottom:0.5%">昨日充值人数:{{ ydayRechargeNum }}</el-col> <el-col style="padding-left: 35%;">其中首充:{{ firstRecharge }}</el-col> </template> </el-card> </div>
<div class="graph"> <el-card style="width:100%;"> <div> <el-tabs v-model="activeTab" @tab-change="handleTabChange"> <el-tab-pane label="金币充值" name="recharge"></el-tab-pane> <el-tab-pane label="金币消费" name="consume"></el-tab-pane> </el-tabs> </div> <div class="condition"> <div style="display:flex;">合计 <span>永久金币:</span> {{ activeTab === 'recharge' ? sumRechargePermanent / 100 : sumConsumePermanent / 100 }} <span>免费金币:</span> {{ activeTab === 'recharge' ? sumRechargeFree / 100 : sumConsumeFree / 100 }} <span>任务金币:</span> {{ activeTab === 'recharge' ? sumRechargeTask / 100 : sumConsumeTask / 100 }} <div v-if="activeTab === 'consume'">合计 {{ sumConsume / 100 }}</div> </div> <div style="margin-left:auto;margin-right: 0.5%;"> <el-button @click="getYes()" :type="activeTimeRange === 'yes' ? 'primary' : ''">昨天 </el-button> <el-button @click="getToday()" :type="activeTimeRange === 'today' ? 'primary' : ''">今天 </el-button> <el-button @click="getWeek()" :type="activeTimeRange === 'week' ? 'primary' : ''">本周 </el-button> <el-button @click="getMonth()" :type="activeTimeRange === 'month' ? 'primary' : ''">本月 </el-button> <el-button @click="getYear()" :type="activeTimeRange === 'year' ? 'primary' : ''">本年 </el-button> </div> <div> <el-date-picker v-model="dateRange" type="datetimerange" range-separator="→" start-placeholder="开始时间" end-placeholder="结束时间" format="YYYY-MM-DD HH:mm:ss" style="width:20vw" value-format="YYYY-MM-DD HH:mm:ss" :default-time="defaultTime" :disabled-date="disabledDate" @change="handleDatePickerChange"/> <el-button type="primary" style="margin-left: 5px" @click="getChartData">查询</el-button> </div> </div>
<div class="graph-content"> <div ref="chartRef" class="left"></div> <div class="right"> <el-card> <div class="card-large">金币{{ activeTab === 'recharge' ? '充值' : '消费' }}排名</div> <el-select v-model="selectedType" style="width: 100%; margin-bottom: 15px"> <el-option label="全部类型" value="all"></el-option> <el-option label="永久金币" value="permanent"></el-option> <el-option label="免费金币" value="free"></el-option> <el-option label="任务金币" value="task"></el-option> </el-select> <el-table :data="tableData" height="320px"> <el-table-column prop="rank" label="排名" width="60" align="center"></el-table-column> <el-table-column prop="market" label="地区" align="center"> <template #default="scope"> <span>{{ marketMapping[scope.row.market] || scope.row.market }}</span> </template> </el-table-column> <el-table-column prop="coinAmount" label="金币数量" align="center"> <template #default="{ row }"> {{ row.coinAmount.toLocaleString() }} </template> </el-table-column> </el-table> </el-card> </div> </div> </el-card> </div> </template>
<script setup> import * as echarts from 'echarts' import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue' import API from '@/util/http' import { ElMessage } from 'element-plus' import dayjs from 'dayjs'; import utc from 'dayjs-plugin-utc' import weekday from 'dayjs/plugin/weekday' dayjs.extend(utc) import { ArrowUpBold, ArrowDownBold, SemiSelect } from '@element-plus/icons-vue' import { marketMapping } from "@/utils/marketMap.js";
const defaultTime = [ new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59), ]
// 地区数据
const markets = ref([]) // 图表相关
const dateRange = ref([]) const activeTab = ref('recharge') const selectedType = ref('all') const tableData = ref([]) const chartRef = ref(null) let chartInstance = null // 图表合计数
const sumRechargePermanent = ref(0) const sumRechargeFree = ref(0) const sumRechargeTask = ref(0) const sumConsumePermanent = ref(0) const sumConsumeFree = ref(0) const sumConsumeTask = ref(0) const sumConsume = ref(0) // 用户信息
const adminData = ref({}) // 卡片数据相关
const currentGold = ref(0) const dailyChange = ref(0) const currentPermanent = ref(0) const currentFree = ref(0) const currentFreeJune = ref(0) const currentFreeDecember = ref(0) const currentTask = ref(0) const yearlyRecharge = ref(0) const yearlyMoney = ref(0) const recharge = ref(0) const money = ref(0) const yearlyReduce = ref(0) const yearlyConsume = ref(0) const yearlyRefund = ref(0) const dailyReduce = ref(0) const dailyConsume = ref(0) const dailyRefund = ref(0) const yearlyRechargeNum = ref(0) const sumWow = ref(0) const sumDaily = ref(0) const rechargeNum = ref(0) const ydayRechargeNum = ref(0) const firstRecharge = ref(0) const length = ref(0) // 加载状态
const chartLoading = ref(true)
const handleResize = () => { if (chartInstance.value) { try { chartInstance.value.resize() console.log('resize一下') } catch (error) { console.error('图表resize失败:', error) } } } // 初始化图表
const initChart = () => { if (!chartInstance && chartRef.value) { chartInstance = echarts.init(chartRef.value) window.addEventListener('resize', handleResize) } } // 销毁图表
const destroyChart = () => { if (chartInstance.value) { try { chartInstance.value.dispose() } catch (error) { console.error('图表销毁失败:', error) } chartInstance.value = null } window.removeEventListener('resize', handleResize) } const formatDate = function (date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } // 昨天
const getYes = function () { const yesterday = dayjs().subtract(1, 'day') const startTime = yesterday.startOf('day').format('YYYY-MM-DD HH:mm:ss') const endTime = yesterday.endOf('day').format('YYYY-MM-DD HH:mm:ss') dateRange.value = [startTime, endTime] console.log('看看dateRange', dateRange.value) activeTimeRange.value = 'yes' // 标记当前激活状态
getChartData() } // 今天
const getToday = function () { const today = dayjs() const startTime = today.startOf('day').format('YYYY-MM-DD HH:mm:ss') const endTime = today.endOf('day').format('YYYY-MM-DD HH:mm:ss') // const endTime = today.add(1, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss')
dateRange.value = [startTime, endTime] console.log('看看dateRange', dateRange.value) activeTimeRange.value = 'today' // 标记当前激活状态
getChartData() } // 本周
const getWeek = function () { const today = dayjs(); // 获取今天是星期几(0是周日,1是周一,...,6是周六)
const day = today.day();
// 计算本周一(如果今天是周一,就取今天;如果是周日,就减6天)
let monday = today.subtract(day === 0 ? 6 : day - 1, 'day'); // 计算本周日(如果今天是周日,就取今天;否则就加(7 - day)天)
let sunday = today.add(day === 0 ? 0 : 7 - day, 'day');
// 设置时间为起始和结束
const startTime = monday.startOf('day').format('YYYY-MM-DD HH:mm:ss'); const endTime = sunday.endOf('day').format('YYYY-MM-DD HH:mm:ss');
dateRange.value = [startTime, endTime]; console.log('本周时间范围(周一到周日):', dateRange.value); activeTimeRange.value = 'week';
getChartData(); }; // 本月
const getMonth = function () { const today = dayjs() const startTime = today.startOf('month').format('YYYY-MM-DD HH:mm:ss') // const endTime = today.add(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss')
const endTime = today.endOf('month').format('YYYY-MM-DD HH:mm:ss') dateRange.value = [startTime, endTime] console.log('看看dateRange', dateRange.value) activeTimeRange.value = 'month' // 标记当前激活状态
getChartData() } // 本年
const getYear = function () { const today = dayjs() const startTime = today.startOf('year').format('YYYY-MM-DD HH:mm:ss') const endTime = today.endOf('year').format('YYYY-MM-DD HH:mm:ss') // const endTime = today.add(1, 'year').startOf('year').format('YYYY-MM-DD HH:mm:ss')
dateRange.value = [startTime, endTime] console.log('看看dateRange', dateRange.value) activeTimeRange.value = 'year' // 标记当前激活状态
getChartData() }
// 要加上所有市场的,还有额外计算的(总数 = 永久 + 6月 + 12月 + 免费 + 任务)
const processData = (data) => { const summary = { currentGold: 0, dailyChange: 0, currentPermanent: 0, currentFreeJune: 0, currentFreeDecember: 0, currentTask: 0, currentFree: 0, recharge: 0, money: 0, yearlyRecharge: 0, yearlyMoney: 0, consumePermanent: 0, consumeFreeJune: 0, consumeFreeDecember: 0, consumeTask: 0, refundPermanent: 0, refundFreeJune: 0, refundFreeDecember: 0, refundTask: 0, dailyReduce: 0, yearlyConsume: 0, yearlyRefund: 0, yearlyReduce: 0, rechargeNum: 0, ydayRechargeNum: 0, firstRecharge: 0, sumWow: 0, sumDaily: 0, yearlyRechargeNum: 0 }
// 遍历市场
data.marketCards.forEach(market => { for (const i in summary) { if (market[i] !== undefined && market[i] !== null) { // 其实还应该卡一个number
summary[i] += market[i] } } })
// wow和daily除一下
length.value = data.markets.length console.log(length.value)
// 计算昨日新增消费和退款
const yesterdayConsume = summary.consumePermanent + summary.consumeFreeJune + summary.consumeFreeDecember + summary.consumeTask const yesterdayRefund = summary.refundPermanent + summary.refundFreeJune + summary.refundFreeDecember + summary.refundTask
// 更新卡片数据
currentGold.value = summary.currentGold.toFixed(2) dailyChange.value = summary.dailyChange.toFixed(2) currentPermanent.value = summary.currentPermanent.toFixed(2) currentFree.value = summary.currentFree.toFixed(2) currentFreeJune.value = summary.currentFreeJune.toFixed(2) currentFreeDecember.value = summary.currentFreeDecember.toFixed(2) currentTask.value = summary.currentTask.toFixed(2)
yearlyRecharge.value = summary.yearlyRecharge.toFixed(2) yearlyMoney.value = summary.yearlyMoney.toFixed(2) recharge.value = summary.recharge.toFixed(2) money.value = summary.money.toFixed(2)
yearlyReduce.value = summary.yearlyReduce.toFixed(2) yearlyConsume.value = summary.yearlyConsume.toFixed(2) yearlyRefund.value = summary.yearlyRefund.toFixed(2) dailyReduce.value = summary.dailyReduce.toFixed(2) dailyConsume.value = yesterdayConsume.toFixed(2) dailyRefund.value = yesterdayRefund.toFixed(2)
yearlyRechargeNum.value = summary.yearlyRechargeNum
// // 周同比
// sumWow.value = (marketCards.sumWow / length.value).toFixed(2)
// // 日环比
// sumDaily.value = (marketCards.sumDaily / length.value).toFixed(2)
// rechargeNum.value = summary.rechargeNum
ydayRechargeNum.value = summary.ydayRechargeNum firstRecharge.value = summary.firstRecharge }
//无法选择的时间
const disabledDate = (time) => { const limitDate = new Date(2025, 0, 1); return time.getTime() < limitDate.getTime(); }
// 获取市场列表
const getMarkets = async () => { console.log("adminData", adminData.value.account) try { const response = await API({ url: '/general/adminMarkets', data: { account: adminData.value.account } }) if (Array.isArray(response.data)) { // markets.value = response.data.filter(data => data !== "1")
markets.value = response.data console.log('市场列表获取成功:', markets.value) } else { console.error('获取市场列表失败', response) ElMessage.error('获取市场列表失败') } } catch (error) { console.error('获取市场列表失败:', error) ElMessage.error('获取市场列表失败') } }
// 获取图表数据
const getChartData = async () => { try { // 校验市场数据到底有没有
if (!markets.value || markets.value.length === 0) { await getMarkets() } // 本年
if (!dateRange.value || dateRange.value.length === 0) { getYear() } const params = { markets: markets.value, startDate: dateRange.value[0], endDate: dateRange.value[1] };
const response = await API({ url: '/workbench/getGraph', data: params }) console.log('看看params', params) if (Array.isArray(response.marketGraphs)) { // const filteredGraphs = response.marketGraphs.filter(data => data.market !== "1");
// 处理图表数据
processChartData(response.marketGraphs) // 处理排名数据
processRankingData(response.marketGraphs) } else { console.error('获取图表数据失败:', response) ElMessage.error('获取图表数据失败') } } catch (error) { console.error('获取图表数据失败:', error) ElMessage.error('获取图表数据失败') } } // 处理图表数据
const processChartData = (marketCards) => { const chartData = { rechargePermanent: [], rechargeFree: [], rechargeTask: [], consumePermanent: [], consumeFree: [], consumeTask: [], sumConsume: [] } // 这是图表的合计数,怎样遍历?????
const sumRechargePermanent1 = ref(0) const sumRechargeFree1 = ref(0) const sumRechargeTask1 = ref(0) const sumConsumePermanent1 = ref(0) const sumConsumeFree1 = ref(0) const sumConsumeTask1 = ref(0) const sumConsume1 = ref(0)
marketCards.forEach(market => { chartData.rechargePermanent.push(market.sumRechargePermanent / 100 || 0) chartData.rechargeFree.push(market.sumRechargeFree / 100 || 0) chartData.rechargeTask.push(market.sumRechargeTask / 100 || 0) chartData.consumePermanent.push(market.sumConsumePermanent / 100 || 0) chartData.consumeFree.push(market.sumConsumeFree / 100 || 0) chartData.consumeTask.push(market.sumConsumeTask / 100 || 0) chartData.sumConsume.push(market.sumConsume / 100 || 0)
// 合计数合计数合计数咋算
sumRechargePermanent1.value += (market.sumRechargePermanent || 0) sumRechargeFree1.value += (market.sumRechargeFree || 0) //sumRechargeTask1.value += (market.sumRechargeTask || 0)
sumConsumePermanent1.value += (market.sumConsumePermanent || 0) sumConsumeFree1.value += (market.sumConsumeFree || 0) sumConsumeTask1.value += (market.sumConsumeTask || 0) sumConsume1.value += (market.sumConsume || 0) }) sumRechargePermanent.value = sumRechargePermanent1.value sumRechargeFree.value = sumRechargeFree1.value sumRechargeTask.value = 0 sumConsumePermanent.value = sumConsumePermanent1.value sumConsumeFree.value = sumConsumeFree1.value sumConsumeTask.value = sumConsumeTask1.value sumConsume.value = sumConsume1.value
updateChart(chartData) }
const processRankingData = (marketCards) => { // 每个市场的总金币数
const rankingData = marketCards.map(market => { let coinAmount = 0; if (activeTab.value === 'recharge') { // 充值排名
switch (selectedType.value) { case 'all': coinAmount = (market.sumRechargePermanent / 100 || 0) + (market.sumRechargeFree / 100 || 0) + (market.sumRechargeTask / 100 || 0); break; case 'permanent': coinAmount = market.sumRechargePermanent / 100 || 0; break; case 'free': coinAmount = market.sumRechargeFree / 100 || 0; break; case 'task': coinAmount = market.sumRechargeTask / 100 || 0; break; } } else { // 消费排名
switch (selectedType.value) { case 'all': coinAmount = (market.sumConsumePermanent / 100 || 0) + (market.sumConsumeFree / 100 || 0) + (market.sumConsumeTask / 100 || 0); break; case 'permanent': coinAmount = market.sumConsumePermanent / 100 || 0; break; case 'free': coinAmount = market.sumConsumeFree / 100 || 0; break; case 'task': coinAmount = market.sumConsumeTask / 100 || 0; break; } } return { market: market.market, coinAmount: coinAmount }; });
// 按金币数量排序
rankingData.sort((a, b) => b.coinAmount - a.coinAmount);
// 排名序号
tableData.value = rankingData.map((item, index) => ({ rank: index + 1, ...item })); }
watch(selectedType, () => { getChartData(); }); // 更新图表
const updateChart = (chartData) => { if (!chartInstance) { initChart() } chartLoading.value = true try { let series = [] let legend = []
if (activeTab.value === 'recharge') { series = [ { name: '永久金币', type: 'bar', stack: 'recharge', data: chartData.rechargePermanent, itemStyle: { color: '#5470c6' }, barWidth: 30 }, { name: '免费金币', type: 'bar', stack: 'recharge', data: chartData.rechargeFree, itemStyle: { color: '#91cc75' }, barWidth: 30 }, { name: '任务金币', type: 'bar', stack: 'recharge', data: chartData.rechargeTask, itemStyle: { color: '#fac858' }, barWidth: 30 } ] legend = ['永久金币', '免费金币', '任务金币'] } else { series = [ { name: '永久金币', type: 'bar', stack: 'consume', data: chartData.consumePermanent, itemStyle: { color: '#5470c6' }, barWidth: 30 }, { name: '免费金币', type: 'bar', stack: 'consume', data: chartData.consumeFree, itemStyle: { color: '#91cc75' }, barWidth: 30 }, { name: '任务金币', type: 'bar', stack: 'consume', data: chartData.consumeTask, itemStyle: { color: '#fac858' }, barWidth: 30 } ] legend = ['永久金币', '免费金币', '任务金币'] }
const option = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: function (params) { let result = params[0].name + '<br/>' let total = 0; params.forEach(param => { result += `${param.seriesName}: ${param.value.toLocaleString()}<br/>`; total += param.value; }) result += `总${activeTab.value === 'recharge' ? '充值' : '消费'}: ${total.toLocaleString()}`; return result } }, legend: { data: legend, bottom: 10 }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, xAxis: { type: 'category', data: markets.value, axisLabel: { interval: 0, rotate: 30 } }, yAxis: { type: 'value', axisLabel: { formatter: function (value) { return value.toLocaleString() } } }, series: series, // dataZoom: [
// {
// type: 'slider',
// show: true,
// start: 0,
// end: 100,
// maxSpan: 100,
// minSpan: 100,
//
// height: 2,
// },
// ]
}
chartInstance.setOption(option) } catch (error) { console.error('图表更新失败:', error) ElMessage.error('图表渲染失败') } finally { setTimeout(() => { chartLoading.value = false }, 300) } }
// 处理标签切换
const handleTabChange = () => { getChartData() console.log('标签切换调用图表') }
const getAdminData = async function () { try { const result = await API({ url: '/admin/userinfo', data: {} }) adminData.value = result console.log('用户信息', adminData.value) } catch (error) { console.log('请求失败', error) } } // 获取卡片数据
const getCardData = async () => { try { const response = await API({ url: '/workbench/getCard', data: {} }) workDataUpdateTime.value = response.updateTime // 周同比
sumWow.value = response.sumWow.toFixed(2) // 日环比
sumDaily.value = response.sumDaily.toFixed(2)
if (response && response.data) { processData(response.data) } else if (Array.isArray(response?.marketCards)) { processData(response) } else { console.error('无效的API响应结构:', response) } } catch (error) { console.error('获取卡片数据失败:', error) } } const workDataUpdateTime = ref(null)
// 标记当前激活的时间范围按钮
const activeTimeRange = ref('') // 日期选择器变化时清除按钮激活状态
const handleDatePickerChange = () => { activeTimeRange.value = '' }
onMounted(async () => { await getAdminData() await getCardData() await getMarkets() getYear() window.addEventListener('resize', () => { chartInstance.resize() }) }) onUnmounted(() => { destroyChart() }) </script>
<style scoped> .top { height: 5.5%; width:100%; display: flex; margin-bottom: 0.2%; .text{ margin-left:0.2%; width:50%; display: flex; align-items: center; font-size: 18px; } }
.card { height: 28%; display: flex; justify-content: center; }
.graph { width: 100%; display: flex; height: 64%;
.condition { width: 100%; height: 1%; display: flex; align-items: center; }
.graph-content { flex: 1; height: auto; display: flex;
.left { width: 70%; height: auto; }
.right { flex: 1; padding: 0.5% 2%; } } }
.center-card { display: flex; justify-content: center; align-items: center; }
.margin-bottom { margin-bottom: 0.5%; }
.card-item { width: 25%; height: 98%; display: flex; flex-direction: column; justify-content: center; margin-right: 0.25%; }
.card-title { font-weight: bold; margin-bottom: 10px; display: flex; justify-content: center; align-items: center; }
.card-large { font-weight: bold; font-size: 16px; text-align: center; margin-bottom: 15px; }
@keyframes spin { 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } } </style>
|