rank_backend/frontend/src/AdminPanel.vue
2025-11-07 16:53:52 +08:00

1178 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
// 响应式数据
const rankingData = ref([])
const loading = ref(false)
const selectedDate = ref('')
const showEditModal = ref(false)
const showAddModal = ref(false)
// 编辑表单数据
const editForm = reactive({
id: null,
title: '',
mix_name: '',
series_author: '',
Manufacturing_Field: '',
Copyright_field: '',
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
})
// 新增表单数据
const addForm = reactive({
title: '',
mix_name: '',
series_author: '',
Manufacturing_Field: '',
Copyright_field: '',
play_vv: 0,
total_likes_formatted: '',
cover_image_url: '',
cover_backup_urls: [],
timeline_data: {
play_vv_change: 0,
play_vv_change_rate: 0
}
})
// 初始化日期为今天
const initDate = () => {
const today = new Date()
selectedDate.value = today.toISOString().split('T')[0]
}
// API基础URL
const API_BASE_URL = 'http://159.75.150.210:8443/api'
// 获取排行榜数据
const fetchRankingData = async () => {
loading.value = true
try {
const response = await axios.get(`${API_BASE_URL}/rank/videos`, {
params: {
page: 1,
limit: 100,
sort: 'growth',
start_date: selectedDate.value,
end_date: selectedDate.value
}
})
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 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 editItem = async (item) => {
editForm.id = item.id || item._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.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
}
// 加载分类状态
await loadClassificationStatus(item.mix_name)
showEditModal.value = true
}
// 加载分类状态
const loadClassificationStatus = async (mixName) => {
try {
const response = await axios.get(`${API_BASE_URL}/rank/get_content_classification`, {
params: { mix_name: mixName }
})
if (response.data.success) {
const classifications = 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_name) {
alert('合集名不能为空')
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_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_name)
}
} catch (error) {
console.error('分类更新失败:', error)
alert('分类更新失败,请检查网络连接')
// 恢复checkbox状态
await loadClassificationStatus(editForm.mix_name)
}
}
// 删除项目
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 = {
title: editForm.title,
mix_name: editForm.mix_name,
series_author: editForm.series_author,
Manufacturing_Field: editForm.Manufacturing_Field,
Copyright_field: editForm.Copyright_field,
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,
target_date: selectedDate.value
}
// 调用后端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 addNewItem = async () => {
try {
const newItemData = {
title: addForm.title,
mix_name: addForm.mix_name,
series_author: addForm.series_author,
Manufacturing_Field: addForm.Manufacturing_Field,
Copyright_field: addForm.Copyright_field,
play_vv: addForm.play_vv,
total_likes_formatted: addForm.total_likes_formatted,
cover_image_url: addForm.cover_image_url,
cover_backup_urls: addForm.cover_backup_urls,
timeline_data: addForm.timeline_data
}
// 尝试调用后端API添加数据
try {
const response = await axios.post(`${API_BASE_URL}/rank/videos`, newItemData)
// 重置表单
Object.keys(addForm).forEach(key => {
if (key === 'play_vv') {
addForm[key] = 0
} else if (key === 'cover_backup_urls') {
addForm[key] = []
} else if (key === 'timeline_data') {
addForm[key] = {
play_vv_change: 0,
play_vv_change_rate: 0
}
} else {
addForm[key] = ''
}
})
showAddModal.value = false
// 重新获取最新数据,确保前端显示的是数据库中的最新数据
await fetchRankingData()
alert('添加成功!')
} catch (apiError) {
console.warn('API添加失败使用本地添加:', apiError)
const newItem = {
id: Date.now(),
...newItemData
}
// 添加到本地数据
rankingData.value.unshift(newItem)
// 重置表单
Object.keys(addForm).forEach(key => {
if (key === 'play_vv') {
addForm[key] = 0
} else if (key === 'cover_backup_urls') {
addForm[key] = []
} else if (key === 'timeline_data') {
addForm[key] = {
play_vv_change: 0,
play_vv_change_rate: 0
}
} else {
addForm[key] = ''
}
})
showAddModal.value = false
alert('添加失败,但本地数据已更新。请检查网络连接。')
}
} catch (error) {
console.error('添加失败:', error)
alert('添加失败!')
}
}
// 取消编辑
const cancelEdit = () => {
showEditModal.value = false
}
// 取消添加
const cancelAdd = () => {
showAddModal.value = false
// 重置表单
Object.keys(addForm).forEach(key => {
if (key === 'play_vv') {
addForm[key] = 0
} else if (key === 'cover_backup_urls') {
addForm[key] = []
} else if (key === 'timeline_data') {
addForm[key] = {
play_vv_change: 0,
play_vv_change_rate: 0
}
} else {
addForm[key] = ''
}
})
}
// 日期改变处理
const onDateChange = () => {
fetchRankingData()
}
// 返回前端页面
const goBack = () => {
router.push('/')
}
// 页面加载时初始化
onMounted(() => {
initDate()
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="showAddModal = true">
添加新项目
</button>
<button class="btn btn-secondary" @click="fetchRankingData">
刷新数据
</button>
</div>
</div>
<!-- 日期选择区域 -->
<div class="date-section">
<label for="date-input">选择日期</label>
<input
id="date-input"
type="date"
v-model="selectedDate"
@change="onDateChange"
class="date-input"
/>
<span class="data-count"> {{ rankingData.length }} 条数据</span>
</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.Manufacturing_Field" type="text" class="form-input" placeholder="请输入承制方" />
</div>
<div class="form-group">
<label>版权方</label>
<input v-model="editForm.Copyright_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.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>封面图片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 v-if="showAddModal" class="modal-overlay" @click="cancelAdd">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>添加新项目</h3>
<button class="close-btn" @click="cancelAdd">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>剧名</label>
<input v-model="addForm.title" type="text" class="form-input" />
</div>
<div class="form-group">
<label>合集名</label>
<input v-model="addForm.mix_name" type="text" class="form-input" />
</div>
<div class="form-group">
<label>剧场名</label>
<input v-model="addForm.series_author" type="text" class="form-input" />
</div>
<div class="form-group">
<label>承制方</label>
<input v-model="addForm.Manufacturing_Field" type="text" class="form-input" />
</div>
<div class="form-group">
<label>版权方</label>
<input v-model="addForm.Copyright_field" type="text" class="form-input" />
</div>
<div class="form-group">
<label>播放量</label>
<input v-model.number="addForm.play_vv" type="number" class="form-input" />
</div>
<div class="form-group">
<label>点赞数格式化</label>
<input v-model="addForm.total_likes_formatted" type="text" class="form-input" />
</div>
<div class="form-group">
<label>封面图片URL</label>
<input v-model="addForm.cover_image_url" type="url" class="form-input" />
</div>
<div class="form-group">
<label>增长量</label>
<input v-model.number="addForm.timeline_data.play_vv_change" type="number" class="form-input" />
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="cancelAdd">取消</button>
<button class="btn btn-primary" @click="addNewItem">添加</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: 375px;
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;
}
/* 日期选择区域 */
.date-section {
padding: 16px;
background: white;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
}
.date-input {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.data-count {
color: #666;
margin-left: auto;
}
/* 管理内容区域 */
.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;
}
.date-section {
padding: 12px;
}
.admin-content {
padding: 12px;
}
.table {
font-size: 11px;
}
.table th,
.table td {
padding: 8px 4px;
}
}
</style>