1042 lines
26 KiB
Vue
1042 lines
26 KiB
Vue
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import axios from 'axios'
|
||
import { API_BASE_URL } from './api/base'
|
||
|
||
const router = useRouter()
|
||
|
||
// 响应式数据
|
||
const rankingData = ref([])
|
||
const loading = ref(false)
|
||
const showEditModal = ref(false)
|
||
|
||
// 编辑表单数据
|
||
const editForm = reactive({
|
||
id: null,
|
||
mix_id: '',
|
||
title: '',
|
||
mix_name: '',
|
||
series_author: '',
|
||
Manufacturing_Field: '',
|
||
Copyright_field: '',
|
||
classification_type: '', // 新增:女频/玄等
|
||
release_date: '', // 新增:上线日期
|
||
play_vv: 0,
|
||
total_likes_formatted: '',
|
||
cover_image_url: '',
|
||
cover_backup_urls: [],
|
||
timeline_data: {
|
||
play_vv_change: 0,
|
||
play_vv_change_rate: 0
|
||
},
|
||
// 分类字段
|
||
isNovel: false,
|
||
isAnime: false,
|
||
isDrama: false,
|
||
// 评论总结字段
|
||
comments_summary: ''
|
||
})
|
||
|
||
// API基础URL(从环境变量导入)
|
||
|
||
// 格式化播放量
|
||
const formatPlayCount = (count) => {
|
||
if (!count) return '0'
|
||
if (count >= 100000000) {
|
||
return (count / 100000000).toFixed(1) + '亿'
|
||
} else if (count >= 10000) {
|
||
return (count / 10000).toFixed(1) + '万'
|
||
}
|
||
return count.toString()
|
||
}
|
||
|
||
// 获取排行榜数据
|
||
const fetchRankingData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const response = await axios.get(`${API_BASE_URL}/rank/videos`, {
|
||
params: {
|
||
page: 1,
|
||
limit: 100,
|
||
sort: 'growth'
|
||
}
|
||
})
|
||
|
||
if (response.data.success) {
|
||
rankingData.value = response.data.data.map((item, index) => ({
|
||
...item,
|
||
id: item._id || item.id || index + 1 // 确保每个项目都有ID
|
||
}))
|
||
} else {
|
||
console.error('获取数据失败:', response.data.message)
|
||
rankingData.value = []
|
||
}
|
||
} catch (error) {
|
||
console.error('API调用失败:', error)
|
||
// 如果API失败,使用模拟数据
|
||
rankingData.value = [
|
||
{
|
||
id: 1,
|
||
title: "测试视频1",
|
||
mix_name: "测试合集1",
|
||
series_author: "测试剧场1",
|
||
play_vv: 1234567,
|
||
total_likes_formatted: "12.3万",
|
||
cover_image_url: "https://via.placeholder.com/150",
|
||
timeline_data: {
|
||
play_vv_change: 50000,
|
||
play_vv_change_rate: 4.2
|
||
}
|
||
},
|
||
{
|
||
id: 2,
|
||
title: "测试视频2",
|
||
mix_name: "测试合集2",
|
||
series_author: "测试剧场2",
|
||
play_vv: 987654,
|
||
total_likes_formatted: "9.8万",
|
||
cover_image_url: "https://via.placeholder.com/150",
|
||
timeline_data: {
|
||
play_vv_change: 30000,
|
||
play_vv_change_rate: 3.1
|
||
}
|
||
}
|
||
]
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 编辑项目
|
||
const editItem = async (item) => {
|
||
editForm.id = item.id || item._id
|
||
editForm.mix_id = item.mix_id || ''
|
||
editForm.title = item.title || ''
|
||
editForm.mix_name = item.mix_name || ''
|
||
editForm.series_author = item.series_author || ''
|
||
editForm.Manufacturing_Field = item.Manufacturing_Field || ''
|
||
editForm.Copyright_field = item.Copyright_field || ''
|
||
editForm.classification_type = item.classification_type || '' // 新增
|
||
editForm.release_date = item.release_date || '' // 新增
|
||
editForm.play_vv = item.play_vv || 0
|
||
editForm.total_likes_formatted = item.total_likes_formatted || ''
|
||
editForm.cover_image_url = item.cover_image_url || ''
|
||
editForm.cover_backup_urls = item.cover_backup_urls || []
|
||
editForm.timeline_data = {
|
||
play_vv_change: item.timeline_data?.play_vv_change || 0,
|
||
play_vv_change_rate: item.timeline_data?.play_vv_change_rate || 0
|
||
}
|
||
editForm.comments_summary = item.comments_summary || ''
|
||
|
||
// 加载分类状态(优先使用 mix_id,兼容 mix_name)
|
||
await loadClassificationStatus(item.mix_id, item.mix_name)
|
||
|
||
showEditModal.value = true
|
||
}
|
||
|
||
// 加载分类状态
|
||
const loadClassificationStatus = async (mixId, mixName) => {
|
||
try {
|
||
const response = await axios.get(`${API_BASE_URL}/rank/get_content_classification`, {
|
||
params: { mix_id: mixId, mix_name: mixName }
|
||
})
|
||
|
||
if (response.data.success) {
|
||
const classifications = response.data.data.classification_status || response.data.data
|
||
editForm.isNovel = classifications.novel || false
|
||
editForm.isAnime = classifications.anime || false
|
||
editForm.isDrama = classifications.drama || false
|
||
}
|
||
} catch (error) {
|
||
console.error('加载分类状态失败:', error)
|
||
// 如果加载失败,重置为false
|
||
editForm.isNovel = false
|
||
editForm.isAnime = false
|
||
editForm.isDrama = false
|
||
}
|
||
}
|
||
|
||
// 更新分类
|
||
const updateClassification = async (classificationType, isChecked) => {
|
||
if (!editForm.mix_id && !editForm.mix_name) {
|
||
alert('缺少短剧标识(mix_id 或 mix_name)')
|
||
return
|
||
}
|
||
|
||
// 如果是选中状态,需要先取消其他分类(实现互斥)
|
||
if (isChecked) {
|
||
// 先在前端更新状态,实现互斥效果
|
||
if (classificationType === 'novel') {
|
||
editForm.isAnime = false
|
||
editForm.isDrama = false
|
||
} else if (classificationType === 'anime') {
|
||
editForm.isNovel = false
|
||
editForm.isDrama = false
|
||
} else if (classificationType === 'drama') {
|
||
editForm.isNovel = false
|
||
editForm.isAnime = false
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await axios.post(`${API_BASE_URL}/rank/update_content_classification`, {
|
||
mix_id: editForm.mix_id,
|
||
mix_name: editForm.mix_name,
|
||
classification_type: classificationType,
|
||
action: isChecked ? 'add' : 'remove',
|
||
exclusive: true // 添加互斥标志
|
||
})
|
||
|
||
if (response.data.success) {
|
||
console.log(`${classificationType}分类更新成功`)
|
||
// 如果后端返回了更新后的分类状态,使用后端数据
|
||
if (response.data.data && response.data.data.classification_status) {
|
||
const status = response.data.data.classification_status
|
||
editForm.isNovel = status.novel || false
|
||
editForm.isAnime = status.anime || false
|
||
editForm.isDrama = status.drama || false
|
||
}
|
||
} else {
|
||
alert(`分类更新失败: ${response.data.message}`)
|
||
// 恢复checkbox状态
|
||
await loadClassificationStatus(editForm.mix_id, editForm.mix_name)
|
||
}
|
||
} catch (error) {
|
||
console.error('分类更新失败:', error)
|
||
alert('分类更新失败,请检查网络连接')
|
||
// 恢复checkbox状态
|
||
await loadClassificationStatus(editForm.mix_id, editForm.mix_name)
|
||
}
|
||
}
|
||
|
||
// 清空评论总结(优先使用 mix_id)
|
||
const clearCommentsSummary = async () => {
|
||
if (!confirm('确定要清空评论总结吗?清空后下次定时任务会重新生成。')) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const today = new Date().toISOString().split('T')[0]
|
||
const requestData = {
|
||
date: today
|
||
}
|
||
|
||
// 优先使用 mix_id,备用 mix_name
|
||
if (editForm.mix_id) {
|
||
requestData.mix_id = editForm.mix_id
|
||
} else {
|
||
requestData.mix_name = editForm.mix_name
|
||
}
|
||
|
||
const response = await axios.post(`${API_BASE_URL}/rank/clear_comments_summary`, requestData)
|
||
|
||
if (response.data.success) {
|
||
editForm.comments_summary = ''
|
||
alert('评论总结已清空')
|
||
} else {
|
||
alert(`清空失败: ${response.data.message}`)
|
||
}
|
||
} catch (error) {
|
||
console.error('清空评论总结失败:', error)
|
||
alert('清空评论总结失败,请检查网络连接')
|
||
}
|
||
}
|
||
|
||
// 删除项目
|
||
const deleteItem = async (item) => {
|
||
if (!confirm(`确定要删除 "${item.title || item.mix_name}" 吗?`)) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const itemId = item.id || item._id
|
||
|
||
// 尝试调用后端API删除数据
|
||
try {
|
||
await axios.delete(`${API_BASE_URL}/rank/videos/${itemId}`)
|
||
|
||
// 重新获取最新数据,确保前端显示的是数据库中的最新数据
|
||
await fetchRankingData()
|
||
|
||
alert('删除成功!')
|
||
} catch (apiError) {
|
||
console.warn('API删除失败,使用本地删除:', apiError)
|
||
|
||
// 从本地数据中删除
|
||
const index = rankingData.value.findIndex(i => (i.id || i._id) === itemId)
|
||
if (index > -1) {
|
||
rankingData.value.splice(index, 1)
|
||
}
|
||
alert('删除失败,但本地数据已更新。请检查网络连接。')
|
||
}
|
||
} catch (error) {
|
||
console.error('删除失败:', error)
|
||
alert('删除失败!')
|
||
}
|
||
}
|
||
|
||
// 保存编辑
|
||
const saveEdit = async () => {
|
||
try {
|
||
const updateData = {
|
||
mix_id: editForm.mix_id,
|
||
title: editForm.title,
|
||
mix_name: editForm.mix_name,
|
||
series_author: editForm.series_author,
|
||
Manufacturing_Field: editForm.Manufacturing_Field,
|
||
Copyright_field: editForm.Copyright_field,
|
||
classification_type: editForm.classification_type,
|
||
release_date: editForm.release_date,
|
||
play_vv: editForm.play_vv,
|
||
total_likes_formatted: editForm.total_likes_formatted,
|
||
cover_image_url: editForm.cover_image_url,
|
||
cover_backup_urls: editForm.cover_backup_urls,
|
||
timeline_data: editForm.timeline_data,
|
||
comments_summary: editForm.comments_summary
|
||
}
|
||
|
||
// 调用后端API更新数据
|
||
try {
|
||
const response = await axios.post(`${API_BASE_URL}/rank/update_drama_info`, updateData)
|
||
|
||
if (response.data.success) {
|
||
console.log('API更新成功:', response.data.message)
|
||
|
||
// 关闭编辑模态框
|
||
showEditModal.value = false
|
||
|
||
// 重新获取最新数据,确保前端显示的是数据库中的最新数据
|
||
await fetchRankingData()
|
||
|
||
alert('保存成功!')
|
||
} else {
|
||
throw new Error(response.data.message || '保存失败')
|
||
}
|
||
} catch (apiError) {
|
||
console.error('API更新失败:', apiError)
|
||
|
||
// 如果API失败,仍然更新本地数据作为备用
|
||
const index = rankingData.value.findIndex(i => (i.id || i._id) === editForm.id)
|
||
if (index > -1) {
|
||
rankingData.value[index] = { ...rankingData.value[index], ...updateData }
|
||
}
|
||
|
||
showEditModal.value = false
|
||
alert('保存到服务器失败,但本地数据已更新。请检查网络连接。')
|
||
}
|
||
} catch (error) {
|
||
console.error('保存失败:', error)
|
||
alert('保存失败:' + error.message)
|
||
}
|
||
}
|
||
|
||
// 取消编辑
|
||
const cancelEdit = () => {
|
||
showEditModal.value = false
|
||
}
|
||
|
||
// 返回前端页面
|
||
const goBack = () => {
|
||
router.push('/')
|
||
}
|
||
|
||
// 跳转到认领申请管理页面
|
||
const goToClaimApplications = () => {
|
||
router.push('/admin/claim-applications')
|
||
}
|
||
|
||
// 页面加载时初始化
|
||
onMounted(() => {
|
||
fetchRankingData()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="admin-panel">
|
||
<!-- 主容器 -->
|
||
<div class="main-container">
|
||
<!-- 顶部标题区域 -->
|
||
<div class="header-section">
|
||
<div class="title-wrapper">
|
||
<button class="back-btn" @click="goBack">←</button>
|
||
<h1 class="main-title">AI棒榜 - 后台管理</h1>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button class="btn btn-primary" @click="goToClaimApplications">
|
||
认领申请管理
|
||
</button>
|
||
<button class="btn btn-secondary" @click="fetchRankingData">
|
||
刷新数据
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理内容区域 -->
|
||
<div class="admin-content">
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading">
|
||
<div class="loading-spinner"></div>
|
||
<p>加载中...</p>
|
||
</div>
|
||
|
||
<!-- 数据表格 -->
|
||
<div v-else class="data-table">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>排名</th>
|
||
<th>封面</th>
|
||
<th>剧名</th>
|
||
<th>剧场名</th>
|
||
<th>承制方</th>
|
||
<th>版权方</th>
|
||
<th>播放量</th>
|
||
<th>点赞数</th>
|
||
<th>增长量</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(item, index) in rankingData" :key="item._id || index"
|
||
class="clickable-row"
|
||
@click="editItem(item)"
|
||
title="点击编辑此短剧">
|
||
<td class="rank-cell">
|
||
<span class="rank-badge" :class="getRankClass(index + 1)">
|
||
{{ index + 1 }}
|
||
</span>
|
||
</td>
|
||
<td class="image-cell">
|
||
<img :src="item.cover_image_url || 'https://via.placeholder.com/150'"
|
||
alt="封面"
|
||
class="cover-image"
|
||
@error="$event.target.src='https://via.placeholder.com/150'" />
|
||
</td>
|
||
<td class="title-cell">
|
||
<div class="title-text">{{ item.title || item.mix_name || '未知' }}</div>
|
||
</td>
|
||
<td>{{ item.series_author || '未知' }}</td>
|
||
<td>{{ item.Manufacturing_Field || '未知' }}</td>
|
||
<td>{{ item.Copyright_field || '未知' }}</td>
|
||
<td>{{ formatPlayCount(item.play_vv) }}</td>
|
||
<td>{{ item.total_likes_formatted || '0' }}</td>
|
||
<td class="growth-cell">
|
||
<span class="growth-value">
|
||
{{ formatPlayCount(item.timeline_data?.play_vv_change || 0) }}
|
||
</span>
|
||
</td>
|
||
<td class="action-cell" @click.stop>
|
||
<button class="btn btn-sm btn-delete" @click="deleteItem(item)">
|
||
删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="rankingData.length === 0" class="empty-state">
|
||
<p>暂无数据</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑模态框 -->
|
||
<div v-if="showEditModal" class="modal-overlay" @click="cancelEdit">
|
||
<div class="modal-content" @click.stop>
|
||
<div class="modal-header">
|
||
<h3>编辑项目</h3>
|
||
<button class="close-btn" @click="cancelEdit">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label>剧名:</label>
|
||
<input v-model="editForm.title" type="text" class="form-input" placeholder="请输入剧名" />
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>合集名:</label>
|
||
<input v-model="editForm.mix_name" type="text" class="form-input" placeholder="请输入合集名" />
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>剧场名:</label>
|
||
<input v-model="editForm.series_author" type="text" class="form-input" placeholder="请输入剧场名" />
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h4 class="section-title">制作信息(锁定字段)</h4>
|
||
<div class="form-group">
|
||
<label>版权方:</label>
|
||
<input v-model="editForm.Copyright_field" type="text" class="form-input" placeholder="请输入版权方(填写后锁定)" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>承制方:</label>
|
||
<input v-model="editForm.Manufacturing_Field" type="text" class="form-input" placeholder="请输入承制方(填写后锁定)" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h4 class="section-title">短剧详细信息(锁定字段)</h4>
|
||
<div class="form-group">
|
||
<label>类型/元素(女频/玄等):</label>
|
||
<input v-model="editForm.classification_type" type="text" class="form-input" placeholder="如:女频/现代(填写后锁定)" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>上线日期:</label>
|
||
<input v-model="editForm.release_date" type="text" class="form-input" placeholder="如:2025年1月1日(填写后锁定)" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h4 class="section-title">数据信息</h4>
|
||
<div class="form-group">
|
||
<label>播放量:</label>
|
||
<input v-model.number="editForm.play_vv" type="number" class="form-input" placeholder="请输入播放量" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>点赞数(格式化):</label>
|
||
<input v-model="editForm.total_likes_formatted" type="text" class="form-input" placeholder="如:1.2万" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>增长量:</label>
|
||
<input v-model.number="editForm.timeline_data.play_vv_change" type="number" class="form-input" placeholder="请输入增长量" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h4 class="section-title">内容分类</h4>
|
||
<div class="classification-group">
|
||
<div class="classification-item">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" v-model="editForm.isNovel" @change="updateClassification('novel', editForm.isNovel)" />
|
||
<span class="checkbox-text">小说</span>
|
||
</label>
|
||
</div>
|
||
<div class="classification-item">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" v-model="editForm.isAnime" @change="updateClassification('anime', editForm.isAnime)" />
|
||
<span class="checkbox-text">动漫</span>
|
||
</label>
|
||
</div>
|
||
<div class="classification-item">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" v-model="editForm.isDrama" @change="updateClassification('drama', editForm.isDrama)" />
|
||
<span class="checkbox-text">短剧</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 评论总结区域 -->
|
||
<div class="form-section">
|
||
<h4 class="section-title">评论总结</h4>
|
||
<div class="form-group">
|
||
<label>评论总结内容:</label>
|
||
<textarea
|
||
v-model="editForm.comments_summary"
|
||
class="form-input"
|
||
rows="6"
|
||
placeholder="评论总结内容(可手动编辑或由系统自动生成)"
|
||
style="resize: vertical;"
|
||
></textarea>
|
||
<button
|
||
v-if="editForm.comments_summary"
|
||
class="btn btn-sm btn-delete"
|
||
@click="clearCommentsSummary"
|
||
style="margin-top: 8px;"
|
||
>
|
||
清空评论总结
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h4 class="section-title">其他信息</h4>
|
||
<div class="form-group">
|
||
<label>封面图片URL:</label>
|
||
<input v-model="editForm.cover_image_url" type="url" class="form-input" placeholder="请输入封面图片URL" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" @click="cancelEdit">取消</button>
|
||
<button class="btn btn-primary" @click="saveEdit">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
methods: {
|
||
getRankClass(rank) {
|
||
if (rank === 1) return 'rank-first'
|
||
if (rank === 2) return 'rank-second'
|
||
if (rank === 3) return 'rank-third'
|
||
return 'rank-normal'
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 全局样式 */
|
||
.admin-panel {
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimHei, Arial, Helvetica, sans-serif;
|
||
}
|
||
|
||
/* 主容器 */
|
||
.main-container {
|
||
max-width: 428px;
|
||
margin: 0 auto;
|
||
background: #f5f5f5;
|
||
min-height: 100vh;
|
||
position: relative;
|
||
}
|
||
|
||
/* 顶部标题区域 */
|
||
.header-section {
|
||
padding: 20px 16px;
|
||
background: white;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.title-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 16px;
|
||
position: relative;
|
||
}
|
||
|
||
.back-btn {
|
||
position: absolute;
|
||
left: 0;
|
||
background: none;
|
||
border: none;
|
||
font-size: 20px;
|
||
color: #4a90e2;
|
||
cursor: pointer;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.3s ease;
|
||
}
|
||
|
||
.back-btn:hover {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.main-title {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin: 0;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 按钮样式 */
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #4a90e2;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #357abd;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #545b62;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.btn-edit {
|
||
background: #28a745;
|
||
color: white;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.btn-edit:hover {
|
||
background: #218838;
|
||
}
|
||
|
||
.btn-delete {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-delete:hover {
|
||
background: #c82333;
|
||
}
|
||
|
||
/* 管理内容区域 */
|
||
.admin-content {
|
||
padding: 16px;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: #666;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 30px;
|
||
height: 30px;
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid #4a90e2;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 16px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* 数据表格 */
|
||
.data-table {
|
||
background: white;
|
||
border-radius: 8px;
|
||
overflow-x: auto;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.table {
|
||
width: 100%;
|
||
min-width: 1100px;
|
||
border-collapse: collapse;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.table th {
|
||
background: #f8f9fa;
|
||
padding: 16px 12px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: #333;
|
||
border-bottom: 2px solid #e0e0e0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.table td {
|
||
padding: 16px 12px;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.table tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.clickable-row {
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.clickable-row:hover {
|
||
background: #e3f2fd !important;
|
||
}
|
||
|
||
/* 表格单元格样式 */
|
||
.rank-cell {
|
||
text-align: center;
|
||
width: 60px;
|
||
}
|
||
|
||
.title-cell {
|
||
width: 200px;
|
||
max-width: 200px;
|
||
}
|
||
|
||
.title-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.rank-badge {
|
||
display: inline-block;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
color: white;
|
||
font-weight: bold;
|
||
font-size: 12px;
|
||
line-height: 24px;
|
||
text-align: center;
|
||
}
|
||
|
||
.rank-badge.rank-first {
|
||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||
color: #333;
|
||
}
|
||
|
||
.rank-badge.rank-second {
|
||
background: linear-gradient(135deg, #c0c0c0, #e8e8e8);
|
||
color: #333;
|
||
}
|
||
|
||
.rank-badge.rank-third {
|
||
background: linear-gradient(135deg, #cd7f32, #daa520);
|
||
}
|
||
|
||
.rank-badge.rank-normal {
|
||
background: #6b7280;
|
||
}
|
||
|
||
.image-cell {
|
||
width: 80px;
|
||
text-align: center;
|
||
}
|
||
|
||
.cover-image {
|
||
width: 60px;
|
||
height: 80px;
|
||
object-fit: cover;
|
||
border-radius: 6px;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.title-cell {
|
||
max-width: 80px;
|
||
}
|
||
|
||
.title-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.growth-cell {
|
||
text-align: center;
|
||
}
|
||
|
||
.growth-value {
|
||
color: #dc3545;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.action-cell {
|
||
width: 80px;
|
||
min-width: 80px;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 空状态 */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 模态框样式 */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
max-width: 350px;
|
||
width: 85%;
|
||
max-height: 85vh;
|
||
overflow: hidden;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.modal-header {
|
||
background: #4a90e2;
|
||
color: white;
|
||
padding: 16px 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
|
||
|
||
.form-group {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.form-section {
|
||
margin-bottom: 24px;
|
||
padding: 16px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
border-left: 4px solid #4a90e2;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 0 0 16px 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #4a90e2;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: #4a90e2;
|
||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||
}
|
||
|
||
/* 分类选择器样式 */
|
||
.classification-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.classification-item {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.checkbox-label {
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
user-select: none;
|
||
}
|
||
|
||
.checkbox-label input[type="checkbox"] {
|
||
margin-right: 8px;
|
||
width: 16px;
|
||
height: 16px;
|
||
cursor: pointer;
|
||
accent-color: #4a90e2;
|
||
}
|
||
|
||
.checkbox-text {
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.checkbox-label:hover .checkbox-text {
|
||
color: #4a90e2;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 16px 20px;
|
||
border-top: 1px solid #e0e0e0;
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 480px) {
|
||
.main-container {
|
||
max-width: 100%;
|
||
}
|
||
|
||
.header-section {
|
||
padding: 16px 12px;
|
||
}
|
||
|
||
.admin-content {
|
||
padding: 12px;
|
||
}
|
||
|
||
.table {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.table th,
|
||
.table td {
|
||
padding: 8px 4px;
|
||
}
|
||
}
|
||
</style> |