991 lines
27 KiB
Vue
991 lines
27 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="reward-rules-page">
|
||
<div class="container">
|
||
<!-- 页面标题 -->
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<h1 class="page-title">
|
||
<Icon name="settings" size="xl" color="#31f08b" 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>
|
||
|
||
<!-- Tab切换 -->
|
||
<a-tabs v-model:activeKey="activeTab" class="reward-tabs">
|
||
<!-- 规则管理 -->
|
||
<a-tab-pane key="rules" tab="规则管理">
|
||
<!-- 筛选 -->
|
||
<div class="filter-bar">
|
||
<a-space>
|
||
<a-select v-model:value="filterType" placeholder="规则类型" style="width: 150px" allowClear @change="loadData">
|
||
<a-select-option v-for="opt in RULE_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
<a-select v-model:value="filterStatus" placeholder="状态" style="width: 120px" allowClear @change="loadData">
|
||
<a-select-option value="1">已启用</a-select-option>
|
||
<a-select-option value="0">已禁用</a-select-option>
|
||
</a-select>
|
||
</a-space>
|
||
</div>
|
||
|
||
<!-- 规则列表 -->
|
||
<div class="rules-list">
|
||
<div v-if="loading" class="loading-container">
|
||
<a-spin size="large" />
|
||
</div>
|
||
<div v-else-if="rules.length === 0" class="empty-state">
|
||
<Icon name="inbox" size="xl" color="#6B6B6B" />
|
||
<p>暂无奖励规则</p>
|
||
<a-button type="primary" @click="showCreateModal">创建规则</a-button>
|
||
</div>
|
||
<div v-else>
|
||
<div v-for="rule in rules" :key="rule.id" class="rule-card">
|
||
<div class="rule-header">
|
||
<div class="rule-info">
|
||
<h3 class="rule-name">{{ rule.name }}</h3>
|
||
<a-tag :color="getRuleTypeColor(rule.ruleType)">
|
||
{{ getRuleTypeName(rule.ruleType) }}
|
||
</a-tag>
|
||
</div>
|
||
<div class="rule-status">
|
||
<a-switch
|
||
:checked="rule.enabled === 1"
|
||
@change="(checked: boolean) => toggleRule(rule, checked)"
|
||
checked-children="启用"
|
||
un-checked-children="禁用"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="rule-desc">{{ rule.description }}</p>
|
||
|
||
<div class="rule-details">
|
||
<div class="detail-item">
|
||
<span class="detail-label">触发条件</span>
|
||
<span class="detail-value">{{ formatCondition(rule.triggerCondition) }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">奖励</span>
|
||
<span class="detail-value reward">+{{ rule.rewardScore }} 积分</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">适用范围</span>
|
||
<span class="detail-value">{{ getScopeName(rule.scope) }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">触发次数</span>
|
||
<span class="detail-value">{{ rule.triggerCount || 0 }} 次</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rule-footer">
|
||
<span class="rule-time">创建于 {{ formatDate(rule.createTime) }}</span>
|
||
<div class="rule-actions">
|
||
<a-button size="small" @click="editRule(rule)">编辑</a-button>
|
||
<a-popconfirm title="确定删除此规则吗?" @confirm="handleDelete(rule.id)">
|
||
<a-button size="small" danger>删除</a-button>
|
||
</a-popconfirm>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div class="pagination-container" v-if="total > 0">
|
||
<a-pagination
|
||
v-model:current="currentPage"
|
||
:total="total"
|
||
:page-size="pageSize"
|
||
show-size-changer
|
||
@change="handlePageChange"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 奖励统计 -->
|
||
<a-tab-pane key="stats" tab="奖励统计">
|
||
<div class="stats-section">
|
||
<!-- 时间筛选 -->
|
||
<div class="stats-filter">
|
||
<a-radio-group v-model:value="statsPeriod" button-style="solid" @change="loadStats">
|
||
<a-radio-button value="today">今日</a-radio-button>
|
||
<a-radio-button value="week">本周</a-radio-button>
|
||
<a-radio-button value="month">本月</a-radio-button>
|
||
</a-radio-group>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="stats-cards">
|
||
<div class="stat-card">
|
||
<div class="stat-value">{{ stats.totalCount || 0 }}</div>
|
||
<div class="stat-label">奖励次数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{{ stats.totalScore || 0 }}</div>
|
||
<div class="stat-label">发放积分</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{{ stats.userCount || 0 }}</div>
|
||
<div class="stat-label">获奖人数</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 排行榜 -->
|
||
<div class="ranking-section">
|
||
<a-row :gutter="24">
|
||
<a-col :span="12">
|
||
<div class="ranking-card">
|
||
<h3 class="ranking-title">规则触发排行</h3>
|
||
<div class="ranking-list">
|
||
<div v-for="(item, index) in (stats.ruleRanking || [])" :key="index" class="ranking-item">
|
||
<span class="rank" :class="{ 'top3': index < 3 }">{{ index + 1 }}</span>
|
||
<span class="name">{{ item.rule_name || item.ruleName }}</span>
|
||
<span class="count">{{ item.trigger_count || item.triggerCount }} 次</span>
|
||
</div>
|
||
<div v-if="!stats.ruleRanking?.length" class="empty-ranking">暂无数据</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<div class="ranking-card">
|
||
<h3 class="ranking-title">用户获奖排行</h3>
|
||
<div class="ranking-list">
|
||
<div v-for="(item, index) in (stats.userRanking || [])" :key="index" class="ranking-item">
|
||
<span class="rank" :class="{ 'top3': index < 3 }">{{ index + 1 }}</span>
|
||
<span class="name">{{ item.user_name || item.userName }}</span>
|
||
<span class="score">+{{ item.total_score || item.totalScore }} 分</span>
|
||
</div>
|
||
<div v-if="!stats.userRanking?.length" class="empty-ranking">暂无数据</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 发放记录 -->
|
||
<a-tab-pane key="records" tab="发放记录">
|
||
<div class="records-section">
|
||
<a-table
|
||
:columns="recordColumns"
|
||
:data-source="records"
|
||
:loading="recordsLoading"
|
||
:pagination="{ current: recordPage, pageSize: 20, total: recordTotal, onChange: loadRecords }"
|
||
row-key="id"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'rewardValue'">
|
||
<span class="reward-value">{{ parseRewardValue(record.rewardValue) }}</span>
|
||
</template>
|
||
<template v-if="column.key === 'createTime'">
|
||
{{ formatDate(record.createTime) }}
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 徽章管理 -->
|
||
<a-tab-pane key="badges" tab="徽章管理">
|
||
<div class="badges-section">
|
||
<div class="badges-grid">
|
||
<div v-for="badge in badges" :key="badge.id" class="badge-card">
|
||
<div class="badge-icon" :style="{ backgroundColor: badge.badgeColor }">
|
||
{{ badge.badgeName.charAt(0) }}
|
||
</div>
|
||
<div class="badge-info">
|
||
<h4>{{ badge.badgeName }}</h4>
|
||
<p>{{ badge.description }}</p>
|
||
<a-tag>{{ badge.category }}</a-tag>
|
||
</div>
|
||
</div>
|
||
<div v-if="!badges.length" class="empty-badges">
|
||
暂无徽章定义
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 称号管理 -->
|
||
<a-tab-pane key="titles" tab="称号管理">
|
||
<div class="titles-section">
|
||
<div class="titles-grid">
|
||
<div v-for="title in titles" :key="title.id" class="title-card">
|
||
<div class="title-name" :style="{ color: title.titleColor }">
|
||
{{ title.titleName }}
|
||
</div>
|
||
<p class="title-desc">{{ title.description }}</p>
|
||
<a-tag>{{ title.category }}</a-tag>
|
||
</div>
|
||
<div v-if="!titles.length" class="empty-titles">
|
||
暂无称号定义
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创建/编辑规则弹窗 -->
|
||
<a-modal
|
||
v-model:open="modalVisible"
|
||
:title="isEdit ? '编辑规则' : '新建规则'"
|
||
width="700px"
|
||
@ok="handleSubmit"
|
||
:confirmLoading="submitting"
|
||
>
|
||
<a-form :model="ruleForm" layout="vertical">
|
||
<a-form-item label="规则名称" required>
|
||
<a-input v-model:value="ruleForm.name" placeholder="例如:每日任务达人" />
|
||
</a-form-item>
|
||
<a-form-item label="规则描述">
|
||
<a-textarea v-model:value="ruleForm.description" :rows="2" placeholder="描述这条规则的作用" />
|
||
</a-form-item>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="规则类型" required>
|
||
<a-select v-model:value="ruleForm.ruleType">
|
||
<a-select-option v-for="opt in RULE_TYPE_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="适用范围" required>
|
||
<a-select v-model:value="ruleForm.scope">
|
||
<a-select-option value="all">全体用户</a-select-option>
|
||
<a-select-option value="designer">设计师</a-select-option>
|
||
<a-select-option value="department">指定部门</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-divider>触发条件</a-divider>
|
||
<a-row :gutter="16">
|
||
<a-col :span="8">
|
||
<a-form-item label="条件类型" required>
|
||
<a-select v-model:value="ruleForm.conditionType">
|
||
<a-select-option v-for="opt in TRIGGER_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="ruleForm.operator">
|
||
<a-select-option v-for="opt in OPERATOR_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-input-number v-model:value="ruleForm.conditionValue" :min="1" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-divider>奖励设置</a-divider>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="奖励积分" required>
|
||
<a-input-number v-model:value="ruleForm.rewardScore" :min="1" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="优先级">
|
||
<a-input-number v-model:value="ruleForm.priority" :min="0" :max="1000" style="width: 100%" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
</a-form>
|
||
</a-modal>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, watch } 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 {
|
||
getRewardRuleList,
|
||
createRewardRule,
|
||
updateRewardRule,
|
||
deleteRewardRule,
|
||
enableRewardRule,
|
||
disableRewardRule,
|
||
getRewardStats,
|
||
getRewardRecords,
|
||
getAllBadges,
|
||
getAllTitles,
|
||
RULE_TYPE_OPTIONS,
|
||
TRIGGER_TYPE_OPTIONS,
|
||
OPERATOR_OPTIONS,
|
||
type RewardRule,
|
||
type RewardStats,
|
||
type RewardRecord,
|
||
type BadgeDefinition,
|
||
type TitleDefinition
|
||
} from '@/api/rewardRule'
|
||
|
||
// Tab状态
|
||
const activeTab = ref('rules')
|
||
|
||
// 规则列表状态
|
||
const loading = ref(false)
|
||
const rules = ref<RewardRule[]>([])
|
||
const total = ref(0)
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(10)
|
||
const filterType = ref<string | undefined>(undefined)
|
||
const filterStatus = ref<string | undefined>(undefined)
|
||
|
||
// 弹窗状态
|
||
const modalVisible = ref(false)
|
||
const isEdit = ref(false)
|
||
const submitting = ref(false)
|
||
const editingId = ref<number | null>(null)
|
||
|
||
// 规则表单
|
||
const ruleForm = ref({
|
||
name: '',
|
||
description: '',
|
||
ruleType: 'daily',
|
||
scope: 'all',
|
||
conditionType: 'task_count',
|
||
operator: '>=',
|
||
conditionValue: 5,
|
||
rewardScore: 20,
|
||
priority: 100
|
||
})
|
||
|
||
// 统计数据
|
||
const statsPeriod = ref('month')
|
||
const stats = ref<Partial<RewardStats>>({})
|
||
|
||
// 发放记录
|
||
const recordsLoading = ref(false)
|
||
const records = ref<RewardRecord[]>([])
|
||
const recordPage = ref(1)
|
||
const recordTotal = ref(0)
|
||
|
||
const recordColumns = [
|
||
{ title: '用户', dataIndex: 'userName', key: 'userName' },
|
||
{ title: '规则', dataIndex: 'ruleName', key: 'ruleName' },
|
||
{ title: '奖励类型', dataIndex: 'rewardType', key: 'rewardType' },
|
||
{ title: '奖励内容', dataIndex: 'rewardValue', key: 'rewardValue' },
|
||
{ title: '发放时间', dataIndex: 'createTime', key: 'createTime' },
|
||
]
|
||
|
||
// 徽章和称号
|
||
const badges = ref<BadgeDefinition[]>([])
|
||
const titles = ref<TitleDefinition[]>([])
|
||
|
||
// 加载规则列表
|
||
const loadData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const result = await getRewardRuleList({
|
||
page: currentPage.value,
|
||
pageSize: pageSize.value,
|
||
ruleType: filterType.value
|
||
})
|
||
rules.value = result.records || []
|
||
total.value = result.total || 0
|
||
} catch (error) {
|
||
console.error('加载规则失败:', error)
|
||
message.error('加载数据失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载统计数据
|
||
const loadStats = async () => {
|
||
try {
|
||
stats.value = await getRewardStats(statsPeriod.value)
|
||
} catch (error) {
|
||
console.error('加载统计失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载发放记录
|
||
const loadRecords = async (page: number = 1) => {
|
||
recordsLoading.value = true
|
||
recordPage.value = page
|
||
try {
|
||
const result = await getRewardRecords({ page, pageSize: 20 })
|
||
records.value = result.records || []
|
||
recordTotal.value = result.total || 0
|
||
} catch (error) {
|
||
console.error('加载记录失败:', error)
|
||
} finally {
|
||
recordsLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载徽章
|
||
const loadBadges = async () => {
|
||
try {
|
||
badges.value = await getAllBadges()
|
||
} catch (error) {
|
||
console.error('加载徽章失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载称号
|
||
const loadTitles = async () => {
|
||
try {
|
||
titles.value = await getAllTitles()
|
||
} catch (error) {
|
||
console.error('加载称号失败:', error)
|
||
}
|
||
}
|
||
|
||
// 监听Tab切换
|
||
watch(activeTab, (newTab) => {
|
||
if (newTab === 'stats') loadStats()
|
||
if (newTab === 'records') loadRecords()
|
||
if (newTab === 'badges') loadBadges()
|
||
if (newTab === 'titles') loadTitles()
|
||
})
|
||
|
||
const showCreateModal = () => {
|
||
isEdit.value = false
|
||
editingId.value = null
|
||
ruleForm.value = {
|
||
name: '',
|
||
description: '',
|
||
ruleType: 'daily',
|
||
scope: 'all',
|
||
conditionType: 'task_count',
|
||
operator: '>=',
|
||
conditionValue: 5,
|
||
rewardScore: 20,
|
||
priority: 100
|
||
}
|
||
modalVisible.value = true
|
||
}
|
||
|
||
const editRule = (rule: RewardRule) => {
|
||
isEdit.value = true
|
||
editingId.value = rule.id
|
||
|
||
let condition = rule.triggerCondition
|
||
if (typeof condition === 'string') {
|
||
condition = JSON.parse(condition)
|
||
}
|
||
|
||
ruleForm.value = {
|
||
name: rule.name,
|
||
description: rule.description || '',
|
||
ruleType: rule.ruleType,
|
||
scope: rule.scope,
|
||
conditionType: condition.type || 'task_count',
|
||
operator: condition.operator || '>=',
|
||
conditionValue: condition.value || 5,
|
||
rewardScore: rule.rewardScore,
|
||
priority: rule.priority
|
||
}
|
||
modalVisible.value = true
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
if (!ruleForm.value.name) {
|
||
message.warning('请输入规则名称')
|
||
return
|
||
}
|
||
|
||
submitting.value = true
|
||
try {
|
||
const data = {
|
||
name: ruleForm.value.name,
|
||
description: ruleForm.value.description,
|
||
ruleType: ruleForm.value.ruleType,
|
||
scope: ruleForm.value.scope,
|
||
triggerCondition: JSON.stringify({
|
||
type: ruleForm.value.conditionType,
|
||
operator: ruleForm.value.operator,
|
||
value: ruleForm.value.conditionValue
|
||
}),
|
||
rewardType: 'score',
|
||
rewardScore: ruleForm.value.rewardScore,
|
||
priority: ruleForm.value.priority
|
||
}
|
||
|
||
if (isEdit.value && editingId.value) {
|
||
await updateRewardRule(editingId.value, data)
|
||
message.success('更新成功')
|
||
} else {
|
||
await createRewardRule(data)
|
||
message.success('创建成功')
|
||
}
|
||
|
||
modalVisible.value = false
|
||
loadData()
|
||
} catch (error) {
|
||
console.error('提交失败:', error)
|
||
message.error('操作失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
const toggleRule = async (rule: RewardRule, enabled: boolean) => {
|
||
try {
|
||
if (enabled) {
|
||
await enableRewardRule(rule.id)
|
||
message.success('规则已启用')
|
||
} else {
|
||
await disableRewardRule(rule.id)
|
||
message.success('规则已禁用')
|
||
}
|
||
loadData()
|
||
} catch (error) {
|
||
message.error('操作失败')
|
||
}
|
||
}
|
||
|
||
const handleDelete = async (id: number) => {
|
||
try {
|
||
await deleteRewardRule(id)
|
||
message.success('删除成功')
|
||
loadData()
|
||
} catch (error) {
|
||
message.error('删除失败')
|
||
}
|
||
}
|
||
|
||
const handlePageChange = (page: number) => {
|
||
currentPage.value = page
|
||
loadData()
|
||
}
|
||
|
||
const getRuleTypeName = (type: string) => {
|
||
const opt = RULE_TYPE_OPTIONS.find(o => o.value === type)
|
||
return opt?.label || type
|
||
}
|
||
|
||
const getRuleTypeColor = (type: string) => {
|
||
const colorMap: Record<string, string> = {
|
||
daily: 'green',
|
||
weekly: 'blue',
|
||
monthly: 'purple',
|
||
task: 'orange',
|
||
rating: 'gold'
|
||
}
|
||
return colorMap[type] || 'default'
|
||
}
|
||
|
||
const getScopeName = (scope: string) => {
|
||
const map: Record<string, string> = {
|
||
all: '全体用户',
|
||
designer: '设计师',
|
||
department: '指定部门'
|
||
}
|
||
return map[scope] || scope
|
||
}
|
||
|
||
const formatCondition = (condition: any) => {
|
||
if (typeof condition === 'string') {
|
||
try {
|
||
condition = JSON.parse(condition)
|
||
} catch {
|
||
return condition
|
||
}
|
||
}
|
||
const typeOpt = TRIGGER_TYPE_OPTIONS.find(o => o.value === condition.type)
|
||
const opOpt = OPERATOR_OPTIONS.find(o => o.value === condition.operator)
|
||
return `${typeOpt?.label || condition.type} ${opOpt?.label || condition.operator} ${condition.value}`
|
||
}
|
||
|
||
const formatDate = (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm')
|
||
|
||
const parseRewardValue = (value: string) => {
|
||
try {
|
||
const obj = JSON.parse(value)
|
||
if (obj.score) return `+${obj.score} 积分`
|
||
return value
|
||
} catch {
|
||
return value
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import "@/styles/variables.scss";
|
||
|
||
.reward-rules-page {
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
background: $bg-primary;
|
||
}
|
||
|
||
.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: $text-primary;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.page-desc {
|
||
color: $text-secondary;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.filter-bar {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.rules-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.rule-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
border-color: $brand-primary;
|
||
}
|
||
}
|
||
|
||
.rule-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.rule-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.rule-name {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.rule-desc {
|
||
color: $text-secondary;
|
||
font-size: 14px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.rule-details {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
padding: 16px;
|
||
background: $bg-secondary;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.detail-label {
|
||
font-size: 12px;
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
.detail-value {
|
||
font-size: 14px;
|
||
color: $text-primary;
|
||
|
||
&.reward {
|
||
color: $brand-primary;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.rule-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-top: 16px;
|
||
border-top: 1px solid $border-primary;
|
||
}
|
||
|
||
.rule-time {
|
||
font-size: 12px;
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
.rule-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pagination-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 48px;
|
||
color: $text-tertiary;
|
||
|
||
p {
|
||
margin: 16px 0;
|
||
}
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 48px;
|
||
}
|
||
|
||
// 统计样式
|
||
.stats-section {
|
||
.stats-filter {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.stats-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 24px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
text-align: center;
|
||
|
||
.stat-value {
|
||
font-size: 36px;
|
||
font-weight: 700;
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
color: $text-secondary;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.ranking-section {
|
||
.ranking-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.ranking-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: $text-primary;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.ranking-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.ranking-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 8px 12px;
|
||
background: $bg-secondary;
|
||
border-radius: 8px;
|
||
|
||
.rank {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
background: $bg-tertiary;
|
||
color: $text-secondary;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
|
||
&.top3 {
|
||
background: $brand-primary;
|
||
color: #000;
|
||
}
|
||
}
|
||
|
||
.name {
|
||
flex: 1;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.count, .score {
|
||
color: $brand-primary;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.empty-ranking {
|
||
text-align: center;
|
||
color: $text-tertiary;
|
||
padding: 24px;
|
||
}
|
||
}
|
||
|
||
// 徽章样式
|
||
.badges-section {
|
||
.badges-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.badge-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
|
||
.badge-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.badge-info {
|
||
flex: 1;
|
||
|
||
h4 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: $text-primary;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
p {
|
||
font-size: 12px;
|
||
color: $text-secondary;
|
||
margin-bottom: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-badges {
|
||
text-align: center;
|
||
color: $text-tertiary;
|
||
padding: 48px;
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
// 称号样式
|
||
.titles-section {
|
||
.titles-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.title-card {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
|
||
.title-name {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.title-desc {
|
||
font-size: 12px;
|
||
color: $text-secondary;
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
|
||
.empty-titles {
|
||
text-align: center;
|
||
color: $text-tertiary;
|
||
padding: 48px;
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
// Tabs样式
|
||
.reward-tabs {
|
||
:deep(.ant-tabs-nav) {
|
||
margin-bottom: 24px;
|
||
}
|
||
}
|
||
|
||
.reward-value {
|
||
color: $brand-primary;
|
||
font-weight: 600;
|
||
}
|
||
</style>
|