使用 Math.round(score * 100) / 100 避免 JavaScript 浮点数计算误差 例如 0.1 × 46 现在正确显示为 4.60 而不是 4.600000000000005
805 lines
24 KiB
Vue
805 lines
24 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="score-pool-page">
|
||
<div class="container">
|
||
<!-- 页面标题 -->
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<h1 class="page-title">
|
||
<Icon name="sparkles" size="xl" color="#FFD700" style="margin-right: 12px;" />
|
||
激励积分池管理
|
||
</h1>
|
||
<p class="page-desc">管理系统激励积分预算,控制奖励发放</p>
|
||
</div>
|
||
<div class="header-right">
|
||
<a-button type="primary" @click="showCreateModal">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
新建积分池
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 当前活跃积分池概览 -->
|
||
<div class="current-pool-card" v-if="currentPool">
|
||
<div class="card-header">
|
||
<h3>
|
||
<Icon name="chartBar" size="md" color="#31f08b" style="margin-right: 8px;" />
|
||
当前活跃积分池
|
||
</h3>
|
||
<a-tag color="green">进行中</a-tag>
|
||
</div>
|
||
<div class="card-title">{{ currentPool.name }}</div>
|
||
|
||
<div class="budget-overview">
|
||
<div class="budget-progress">
|
||
<div class="progress-info">
|
||
<span class="used">已使用 {{ currentPool.usedScore.toLocaleString() }}</span>
|
||
<span class="total">/ {{ currentPool.totalBudget.toLocaleString() }} 积分</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="Math.round((currentPool.usedScore / currentPool.totalBudget) * 100)"
|
||
:stroke-color="getProgressColor(currentPool)"
|
||
:trail-color="'#3A3A3A'"
|
||
:show-info="false"
|
||
/>
|
||
<div class="progress-percent">
|
||
{{ Math.round((currentPool.usedScore / currentPool.totalBudget) * 100) }}%
|
||
</div>
|
||
</div>
|
||
<div class="remaining-score">
|
||
<span class="remaining-label">剩余可用</span>
|
||
<span class="remaining-value">{{ currentPool.remainingScore.toLocaleString() }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="category-breakdown">
|
||
<div class="category-item">
|
||
<div class="category-header">
|
||
<Icon name="document" size="sm" color="#3B82F6" />
|
||
<span>任务奖励</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="Math.round((currentPool.taskRewardUsed / currentPool.taskRewardBudget) * 100)"
|
||
size="small"
|
||
:stroke-color="'#3B82F6'"
|
||
/>
|
||
<div class="category-info">
|
||
{{ currentPool.taskRewardUsed.toLocaleString() }} / {{ currentPool.taskRewardBudget.toLocaleString() }}
|
||
</div>
|
||
</div>
|
||
<div class="category-item">
|
||
<div class="category-header">
|
||
<Icon name="star" size="sm" color="#F59E0B" />
|
||
<span>好评奖励</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="Math.round((currentPool.ratingRewardUsed / currentPool.ratingRewardBudget) * 100)"
|
||
size="small"
|
||
:stroke-color="'#F59E0B'"
|
||
/>
|
||
<div class="category-info">
|
||
{{ currentPool.ratingRewardUsed.toLocaleString() }} / {{ currentPool.ratingRewardBudget.toLocaleString() }}
|
||
</div>
|
||
</div>
|
||
<div class="category-item">
|
||
<div class="category-header">
|
||
<Icon name="trophy" size="sm" color="#EF4444" />
|
||
<span>竞赛奖励</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="Math.round((currentPool.contestRewardUsed / currentPool.contestRewardBudget) * 100)"
|
||
size="small"
|
||
:stroke-color="'#EF4444'"
|
||
/>
|
||
<div class="category-info">
|
||
{{ currentPool.contestRewardUsed.toLocaleString() }} / {{ currentPool.contestRewardBudget.toLocaleString() }}
|
||
</div>
|
||
</div>
|
||
<div class="category-item">
|
||
<div class="category-header">
|
||
<Icon name="calendar" size="sm" color="#31f08b" />
|
||
<span>签到奖励</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="Math.round((currentPool.checkinRewardUsed / currentPool.checkinRewardBudget) * 100)"
|
||
size="small"
|
||
:stroke-color="'#31f08b'"
|
||
/>
|
||
<div class="category-info">
|
||
{{ currentPool.checkinRewardUsed.toLocaleString() }} / {{ currentPool.checkinRewardBudget.toLocaleString() }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-actions">
|
||
<a-button @click="viewPoolDetail(currentPool)">查看详情</a-button>
|
||
<a-button @click="viewPoolRecords(currentPool)">使用记录</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 积分池列表 -->
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">所有积分池</h2>
|
||
<div class="section-filters">
|
||
<a-select v-model:value="filterYear" placeholder="年份" style="width: 120px" allowClear @change="loadData">
|
||
<a-select-option v-for="y in yearOptions" :key="y" :value="y">{{ y }}年</a-select-option>
|
||
</a-select>
|
||
<a-select v-model:value="filterType" placeholder="类型" style="width: 120px" allowClear @change="loadData">
|
||
<a-select-option v-for="opt in POOL_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="columns"
|
||
:data-source="pools"
|
||
:loading="loading"
|
||
:pagination="pagination"
|
||
@change="handleTableChange"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'name'">
|
||
<span class="pool-name">{{ record.name }}</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'budget'">
|
||
<div class="budget-cell">
|
||
<span class="used">{{ record.usedScore.toLocaleString() }}</span>
|
||
<span class="separator">/</span>
|
||
<span class="total">{{ record.totalBudget.toLocaleString() }}</span>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="column.key === 'usage'">
|
||
<a-progress
|
||
:percent="Math.round((record.usedScore / record.totalBudget) * 100)"
|
||
size="small"
|
||
:stroke-color="getProgressColor(record)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'status'">
|
||
<a-tag :color="getStatusColor(record.status)">
|
||
{{ getStatusName(record.status) }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="viewPoolDetail(record)">详情</a-button>
|
||
<a-button type="link" size="small" @click="viewPoolRecords(record)">记录</a-button>
|
||
<template v-if="record.status === 'active'">
|
||
<a-popconfirm title="确定关闭此积分池?" @confirm="handleClosePool(record.id)">
|
||
<a-button type="link" size="small" danger>关闭</a-button>
|
||
</a-popconfirm>
|
||
</template>
|
||
<template v-else-if="record.status === 'closed'">
|
||
<a-button type="link" size="small" @click="handleSettlePool(record.id)">结算</a-button>
|
||
</template>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创建积分池弹窗 -->
|
||
<a-modal
|
||
v-model:open="createModalVisible"
|
||
title="新建积分池"
|
||
width="700px"
|
||
@ok="handleCreatePool"
|
||
:confirmLoading="creating"
|
||
>
|
||
<a-form :model="poolForm" layout="vertical">
|
||
<a-form-item label="积分池名称" required>
|
||
<a-input v-model:value="poolForm.name" placeholder="例如:2025年1月激励积分池" />
|
||
</a-form-item>
|
||
<a-row :gutter="16">
|
||
<a-col :span="8">
|
||
<a-form-item label="积分池类型" required>
|
||
<a-select v-model:value="poolForm.poolType" @change="handlePoolTypeChange">
|
||
<a-select-option v-for="opt in POOL_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8">
|
||
<a-form-item label="年份" required>
|
||
<a-select v-model:value="poolForm.year">
|
||
<a-select-option v-for="y in yearOptions" :key="y" :value="y">{{ y }}年</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8" v-if="poolForm.poolType === 'monthly'">
|
||
<a-form-item label="月份" required>
|
||
<a-select v-model:value="poolForm.month">
|
||
<a-select-option v-for="m in 12" :key="m" :value="m">{{ m }}月</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8" v-if="poolForm.poolType === 'quarterly'">
|
||
<a-form-item label="季度" required>
|
||
<a-select v-model:value="poolForm.quarter">
|
||
<a-select-option v-for="q in 4" :key="q" :value="q">第{{ q }}季度</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-divider>预算设置</a-divider>
|
||
<a-form-item label="总预算积分" required>
|
||
<a-input-number v-model:value="poolForm.totalBudget" :min="1000" :step="10000" style="width: 100%" />
|
||
</a-form-item>
|
||
<a-row :gutter="16">
|
||
<a-col :span="8">
|
||
<a-form-item label="任务奖励预算">
|
||
<a-input-number v-model:value="poolForm.taskRewardBudget" :min="0" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8">
|
||
<a-form-item label="好评奖励预算">
|
||
<a-input-number v-model:value="poolForm.ratingRewardBudget" :min="0" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8">
|
||
<a-form-item label="竞赛奖励预算">
|
||
<a-input-number v-model:value="poolForm.contestRewardBudget" :min="0" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="超支处理策略">
|
||
<a-select v-model:value="poolForm.overflowStrategy">
|
||
<a-select-option v-for="opt in OVERFLOW_STRATEGY_OPTIONS" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="预警阈值 (%)">
|
||
<a-input-number v-model:value="poolForm.alertThreshold" :min="50" :max="100" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 积分池详情弹窗 -->
|
||
<a-modal
|
||
v-model:open="detailModalVisible"
|
||
:title="selectedPool?.name"
|
||
width="800px"
|
||
:footer="null"
|
||
>
|
||
<div v-if="selectedPool" class="pool-detail">
|
||
<a-descriptions :column="2" bordered>
|
||
<a-descriptions-item label="积分池类型">
|
||
{{ getPoolTypeName(selectedPool.poolType) }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="状态">
|
||
<a-tag :color="getStatusColor(selectedPool.status)">
|
||
{{ getStatusName(selectedPool.status) }}
|
||
</a-tag>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="总预算">
|
||
{{ selectedPool.totalBudget.toLocaleString() }} 积分
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="已使用">
|
||
{{ selectedPool.usedScore.toLocaleString() }} 积分
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="剩余">
|
||
{{ selectedPool.remainingScore.toLocaleString() }} 积分
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="使用率">
|
||
{{ Math.round((selectedPool.usedScore / selectedPool.totalBudget) * 100) }}%
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="超支策略">
|
||
{{ getStrategyName(selectedPool.overflowStrategy) }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="预警阈值">
|
||
{{ selectedPool.alertThreshold }}%
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<!-- 使用记录弹窗 -->
|
||
<a-modal
|
||
v-model:open="recordsModalVisible"
|
||
:title="`${selectedPool?.name} - 使用记录`"
|
||
width="900px"
|
||
:footer="null"
|
||
>
|
||
<a-table
|
||
:columns="recordColumns"
|
||
:data-source="poolRecords"
|
||
:loading="loadingRecords"
|
||
:pagination="recordPagination"
|
||
@change="handleRecordTableChange"
|
||
row-key="id"
|
||
size="small"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'scoreChange'">
|
||
<span :class="record.scoreChange > 0 ? 'text-green' : 'text-red'">
|
||
{{ record.scoreChange > 0 ? '+' : '' }}{{ formatScore(record.scoreChange) }}
|
||
</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'rewardType'">
|
||
{{ getRewardTypeName(record.rewardType) }}
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</a-modal>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import { message } from 'ant-design-vue'
|
||
import dayjs from 'dayjs'
|
||
import {
|
||
getScorePoolList,
|
||
getCurrentActivePool,
|
||
createScorePool,
|
||
closeScorePool,
|
||
settleScorePool,
|
||
getPoolRecords,
|
||
POOL_TYPE_OPTIONS,
|
||
OVERFLOW_STRATEGY_OPTIONS,
|
||
type ScorePool,
|
||
type ScorePoolRecord
|
||
} from '@/api/scorePool'
|
||
|
||
const loading = ref(false)
|
||
const pools = ref<ScorePool[]>([])
|
||
const currentPool = ref<ScorePool | null>(null)
|
||
const filterYear = ref<number | undefined>(new Date().getFullYear())
|
||
const filterType = ref<string | undefined>(undefined)
|
||
|
||
const pagination = ref({
|
||
current: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
})
|
||
|
||
const yearOptions = computed(() => {
|
||
const currentYear = new Date().getFullYear()
|
||
return [currentYear - 1, currentYear, currentYear + 1]
|
||
})
|
||
|
||
const columns = [
|
||
{ title: '名称', key: 'name', dataIndex: 'name' },
|
||
{ title: '预算使用', key: 'budget', width: 180 },
|
||
{ title: '使用率', key: 'usage', width: 150 },
|
||
{ title: '状态', key: 'status', width: 100 },
|
||
{ title: '操作', key: 'actions', width: 200 }
|
||
]
|
||
|
||
const recordColumns = [
|
||
{ title: '时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
|
||
{ title: '用户', dataIndex: 'userName', key: 'userName', width: 100 },
|
||
{ title: '类型', key: 'rewardType', width: 100 },
|
||
{ title: '积分', key: 'scoreChange', width: 80 },
|
||
{ title: '池剩余', dataIndex: 'afterRemaining', key: 'afterRemaining', width: 100 },
|
||
{ title: '备注', dataIndex: 'remark', key: 'remark' }
|
||
]
|
||
|
||
const createModalVisible = ref(false)
|
||
const creating = ref(false)
|
||
const poolForm = ref({
|
||
name: '',
|
||
poolType: 'monthly',
|
||
year: new Date().getFullYear(),
|
||
month: new Date().getMonth() + 1,
|
||
quarter: 1,
|
||
totalBudget: 100000,
|
||
taskRewardBudget: 40000,
|
||
ratingRewardBudget: 20000,
|
||
contestRewardBudget: 15000,
|
||
overflowStrategy: 'alert',
|
||
alertThreshold: 80
|
||
})
|
||
|
||
const detailModalVisible = ref(false)
|
||
const selectedPool = ref<ScorePool | null>(null)
|
||
|
||
const recordsModalVisible = ref(false)
|
||
const loadingRecords = ref(false)
|
||
const poolRecords = ref<ScorePoolRecord[]>([])
|
||
const recordPagination = ref({
|
||
current: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
})
|
||
|
||
// 格式化积分(保留两位小数,修复浮点数精度问题)
|
||
const formatScore = (score: number | null | undefined): string => {
|
||
if (score === null || score === undefined) return '0.00'
|
||
// 先乘以100,四舍五入取整,再除以100,避免浮点数精度问题
|
||
return (Math.round(Number(score) * 100) / 100).toFixed(2)
|
||
}
|
||
|
||
const loadData = async () => {
|
||
loading.value = true
|
||
try {
|
||
// 加载当前活跃池
|
||
currentPool.value = await getCurrentActivePool()
|
||
|
||
// 加载列表
|
||
const result = await getScorePoolList({
|
||
page: pagination.value.current,
|
||
pageSize: pagination.value.pageSize,
|
||
poolType: filterType.value,
|
||
year: filterYear.value
|
||
})
|
||
pools.value = result.records || []
|
||
pagination.value.total = result.total || 0
|
||
} catch (error) {
|
||
console.error('加载积分池失败:', error)
|
||
message.error('加载数据失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const showCreateModal = () => {
|
||
const now = new Date()
|
||
poolForm.value = {
|
||
name: `${now.getFullYear()}年${now.getMonth() + 1}月激励积分池`,
|
||
poolType: 'monthly',
|
||
year: now.getFullYear(),
|
||
month: now.getMonth() + 1,
|
||
quarter: 1,
|
||
totalBudget: 100000,
|
||
taskRewardBudget: 40000,
|
||
ratingRewardBudget: 20000,
|
||
contestRewardBudget: 15000,
|
||
overflowStrategy: 'alert',
|
||
alertThreshold: 80
|
||
}
|
||
createModalVisible.value = true
|
||
}
|
||
|
||
const handlePoolTypeChange = () => {
|
||
const type = poolForm.value.poolType
|
||
const year = poolForm.value.year
|
||
if (type === 'monthly') {
|
||
poolForm.value.name = `${year}年${poolForm.value.month}月激励积分池`
|
||
} else if (type === 'quarterly') {
|
||
poolForm.value.name = `${year}年第${poolForm.value.quarter}季度激励积分池`
|
||
} else if (type === 'yearly') {
|
||
poolForm.value.name = `${year}年度激励积分池`
|
||
}
|
||
}
|
||
|
||
const handleCreatePool = async () => {
|
||
if (!poolForm.value.name || !poolForm.value.totalBudget) {
|
||
message.warning('请填写必填项')
|
||
return
|
||
}
|
||
|
||
creating.value = true
|
||
try {
|
||
await createScorePool(poolForm.value)
|
||
message.success('创建成功')
|
||
createModalVisible.value = false
|
||
loadData()
|
||
} catch (error: any) {
|
||
message.error(error.message || '创建失败')
|
||
} finally {
|
||
creating.value = false
|
||
}
|
||
}
|
||
|
||
const viewPoolDetail = (pool: ScorePool) => {
|
||
selectedPool.value = pool
|
||
detailModalVisible.value = true
|
||
}
|
||
|
||
const viewPoolRecords = async (pool: ScorePool) => {
|
||
selectedPool.value = pool
|
||
recordsModalVisible.value = true
|
||
loadingRecords.value = true
|
||
|
||
try {
|
||
const result = await getPoolRecords(pool.id, {
|
||
page: recordPagination.value.current,
|
||
pageSize: recordPagination.value.pageSize
|
||
})
|
||
poolRecords.value = result.records || []
|
||
recordPagination.value.total = result.total || 0
|
||
} catch (error) {
|
||
message.error('加载记录失败')
|
||
} finally {
|
||
loadingRecords.value = false
|
||
}
|
||
}
|
||
|
||
const handleClosePool = async (id: number) => {
|
||
try {
|
||
await closeScorePool(id)
|
||
message.success('积分池已关闭')
|
||
loadData()
|
||
} catch (error: any) {
|
||
message.error(error.message || '操作失败')
|
||
}
|
||
}
|
||
|
||
const handleSettlePool = async (id: number) => {
|
||
try {
|
||
await settleScorePool(id)
|
||
message.success('积分池已结算')
|
||
loadData()
|
||
} catch (error: any) {
|
||
message.error(error.message || '操作失败')
|
||
}
|
||
}
|
||
|
||
const handleTableChange = (pag: any) => {
|
||
pagination.value.current = pag.current
|
||
pagination.value.pageSize = pag.pageSize
|
||
loadData()
|
||
}
|
||
|
||
const handleRecordTableChange = async (pag: any) => {
|
||
recordPagination.value.current = pag.current
|
||
if (selectedPool.value) {
|
||
await viewPoolRecords(selectedPool.value)
|
||
}
|
||
}
|
||
|
||
const getProgressColor = (pool: ScorePool) => {
|
||
const percent = (pool.usedScore / pool.totalBudget) * 100
|
||
if (percent >= 90) return '#EF4444'
|
||
if (percent >= 70) return '#F59E0B'
|
||
return '#31f08b'
|
||
}
|
||
|
||
const getStatusColor = (status: string) => {
|
||
const map: Record<string, string> = {
|
||
active: 'green',
|
||
closed: 'orange',
|
||
settled: 'default'
|
||
}
|
||
return map[status] || 'default'
|
||
}
|
||
|
||
const getStatusName = (status: string) => {
|
||
const map: Record<string, string> = {
|
||
active: '进行中',
|
||
closed: '已关闭',
|
||
settled: '已结算'
|
||
}
|
||
return map[status] || status
|
||
}
|
||
|
||
const getPoolTypeName = (type: string) => {
|
||
const opt = POOL_TYPE_OPTIONS.find(o => o.value === type)
|
||
return opt?.label || type
|
||
}
|
||
|
||
const getStrategyName = (strategy: string) => {
|
||
const opt = OVERFLOW_STRATEGY_OPTIONS.find(o => o.value === strategy)
|
||
return opt?.label || strategy
|
||
}
|
||
|
||
const getRewardTypeName = (type: string) => {
|
||
const map: Record<string, string> = {
|
||
task: '任务奖励',
|
||
rating: '好评奖励',
|
||
contest: '竞赛奖励',
|
||
checkin: '签到奖励',
|
||
goal: '目标奖励',
|
||
other: '其他'
|
||
}
|
||
return map[type] || type
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.score-pool-page {
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.page-desc {
|
||
color: #AFAFAF;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.current-pool-card {
|
||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||
border: 1px solid #3A3A3A;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
|
||
h3 {
|
||
color: #AFAFAF;
|
||
font-size: 14px;
|
||
font-weight: normal;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.budget-overview {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.budget-progress {
|
||
flex: 1;
|
||
margin-right: 40px;
|
||
}
|
||
|
||
.progress-info {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
|
||
.used {
|
||
color: #31f08b;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.total {
|
||
color: #AFAFAF;
|
||
}
|
||
}
|
||
|
||
.progress-percent {
|
||
text-align: right;
|
||
color: #AFAFAF;
|
||
font-size: 13px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.remaining-score {
|
||
text-align: right;
|
||
|
||
.remaining-label {
|
||
display: block;
|
||
color: #6B6B6B;
|
||
font-size: 12px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.remaining-value {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
color: #FFD700;
|
||
}
|
||
}
|
||
|
||
.category-breakdown {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.category-item {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.category-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
color: #ECECEC;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.category-info {
|
||
font-size: 12px;
|
||
color: #6B6B6B;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #ECECEC;
|
||
}
|
||
|
||
.section-filters {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.pool-name {
|
||
font-weight: 500;
|
||
color: #ECECEC;
|
||
}
|
||
|
||
.budget-cell {
|
||
.used {
|
||
color: #31f08b;
|
||
font-weight: 500;
|
||
}
|
||
.separator {
|
||
color: #6B6B6B;
|
||
margin: 0 4px;
|
||
}
|
||
.total {
|
||
color: #AFAFAF;
|
||
}
|
||
}
|
||
|
||
.text-green {
|
||
color: #31f08b;
|
||
}
|
||
|
||
.text-red {
|
||
color: #EF4444;
|
||
}
|
||
</style>
|