加入了node_modules

添加了新的功能项
This commit is contained in:
zyb
2025-05-13 21:23:41 +08:00
parent 8d53374568
commit 0c0b5d869c
3589 changed files with 893641 additions and 233 deletions

View File

@@ -8,23 +8,21 @@
<div class="nav-container">
<div class="nav-left">
<div class="nav-brand">红色警戒3数据分析中心</div>
<router-link to="/" class="nav-link">热门下载地图</router-link>
<<<<<<< HEAD
<router-link to="/weekly" class="nav-link">每周推荐</router-link>
<router-link to="/" class="nav-link">最近上传地图</router-link>
<router-link to="/weekly" class="nav-link">热门下载地图</router-link>
<router-link to="/weapon-match" class="nav-link">Weapon 匹配</router-link>
<router-link to="/demands" class="nav-link">办事大厅</router-link>
<router-link to="/weapon-match" class="nav-link">Weapon 匹配</router-link>
=======
<router-link to="/weekly" class="nav-link">每周推荐</router-link>
<router-link to="/weapon-match" class="nav-link">Weapon 匹配</router-link>
>>>>>>> 062550d1260d3f66fd69a2d954f426abb5531511
</div>
</div>
</nav>
<main class="main-content">
<router-view></router-view>
</main>
<footer class="footer">
<div class="footer-bottom">
<p>Byz解忧杂货铺</p>
</div>
</footer>
</div>
</template>
@@ -105,7 +103,7 @@ body {
margin-top: 60px;
padding: 20px;
flex: 1;
max-width: 1200px;
max-width: 1400px;
width: 100%;
margin-left: auto;
margin-right: auto;
@@ -133,4 +131,55 @@ body {
padding: 15px;
}
}
.footer {
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
color: white;
padding: 40px 0 20px;
margin-top: auto;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 40px;
}
.footer-section h3 {
font-size: 1.2rem;
margin-bottom: 15px;
color: white;
}
.footer-section p {
margin: 8px 0;
color: rgba(255, 255, 255, 0.9);
font-size: 0.9rem;
}
.footer-bottom {
text-align: center;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.footer-bottom p {
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
}
@media (max-width: 768px) {
.footer-content {
grid-template-columns: 1fr;
gap: 30px;
}
.footer {
padding: 30px 0 15px;
}
}
</style>

View File

@@ -81,4 +81,45 @@
margin: 0 -20px;
border-radius: 0;
}
}
/* 渐变按钮基础样式 */
.gradient-btn {
display: inline-block;
padding: 10px 20px;
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-decoration: none;
text-align: center;
}
.gradient-btn:hover {
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.gradient-btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 按钮变体 */
.gradient-btn.block {
display: block;
width: 100%;
}
.gradient-btn.left {
margin: 20px 0;
}
.gradient-btn.center {
margin: 20px auto;
}

View File

@@ -1,7 +1,7 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/styles/common.css'
const app = createApp(App)
app.use(router)

View File

@@ -2,14 +2,11 @@ import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
<<<<<<< HEAD
path: '/demands',
name: 'DemandList',
component: () => import('../views/DemandList.vue')
},
{
=======
>>>>>>> 062550d1260d3f66fd69a2d954f426abb5531511
path: '/',
name: 'Maps',
component: () => import('../views/Maps.vue')

View File

@@ -1,221 +1,531 @@
<template>
<div class="demand-hall">
<header>
<h1>办事大厅</h1>
</header>
<div class="app-content">
<div v-if="loading" class="loading">加载中...</div>
<div v-if="error" class="error">{{ error }}</div>
<div class="demand-list">
<div v-for="demand in demands" :key="demand.id || demand.requester + demand.content" class="demand-card">
<div class="demand-header">
<span class="requester">{{ demand.requester || '匿名' }}</span>
<span class="reward" :class="{ 'no-reward': isNoReward(demand.reward) }">
{{ demand.reward || '无赏金' }}
<div class="demand-hall">
<div class="page-header">
<h1>需求列表</h1>
</div>
<button class="btn-common btn-gradient btn-margin-right" @click="openAddModal">添加需求</button>
<button class="btn-common btn-light" @click="fetchDemands">刷新</button>
<div class="table-container">
<table class="maps-table">
<thead>
<tr>
<th>ID</th>
<th>请求者</th>
<th>QQ号</th>
<th>请求内容</th>
<th>悬赏金额</th>
<th>需求创建时间</th>
</tr>
</thead>
<tbody>
<tr v-for="(demand, index) in demands" :key="demand.id" class="table-row" @click="showDetail(demand)">
<td class="id">{{ index + 1 }}</td>
<td class="requester">
<span v-if="!demand.requester" class="tag no-reward">匿名</span>
<span v-else>{{ demand.requester }}</span></td>
<td class="name">
<span v-if="!demand.qq_code" class="tag no-reward">匿名</span>
<span v-else>{{ demand.qq_code }}</span>
</td>
<td class="content">{{ demand.content }}</td>
<td class="reward">
<span :class="['tag', isNoReward(demand.reward) ? 'no-reward' : 'has-reward']">
{{ isNoReward(demand.reward) ? '无赏金' : demand.reward }}
</span>
</div>
<div class="content">{{ demand.content || '无具体内容' }}</div>
<div class="date">{{ formatDate(demand.date) }}</div>
</td>
<td class="date">{{ formatDate(demand.date) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 详情弹窗 -->
<div v-if="showModal" class="modal-overlay" @click="closeModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h2>需求详情</h2>
<button class="close-btn" @click="closeModal">&times;</button>
</div>
<div class="modal-body">
<div class="detail-item">
<span class="label">QQ号</span>
<span v-if="!selectedDemand?.qq_code" class="tag no-reward">匿名</span>
<span v-else>{{ selectedDemand?.qq_code }}</span>
</div>
<div class="detail-item">
<span class="label">请求者</span>
<span v-if="!selectedDemand?.requester" class="tag no-reward">匿名</span>
<span v-else>{{ selectedDemand?.requester }}</span>
</div>
<div class="detail-item">
<span class="label">需求内容</span>
<span class="value">{{ selectedDemand?.content }}</span>
</div>
<div class="detail-item">
<span class="label">赏金</span>
<span :class="['tag', isNoReward(selectedDemand?.reward) ? 'no-reward' : 'has-reward']">
{{ isNoReward(selectedDemand?.reward) ? '无赏金' : selectedDemand?.reward }}
</span>
</div>
<div class="detail-item">
<span class="label">发布时间</span>
<span class="value">{{ formatDate(selectedDemand?.date) }}</span>
</div>
</div>
<button class="refresh-btn" @click="fetchDemands">刷新列表</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 响应式数据
const demands = ref([])
const loading = ref(false)
const error = ref(null)
// 判断是否为无赏金
const isNoReward = (reward) => {
return !reward || reward === '无赏金'
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString || dateString === 'Test_date') return '日期未提供'
try {
const date = new Date(dateString)
if (isNaN(date.getTime())) {
return dateString
}
const pad = num => num.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
} catch (e) {
<!-- 添加需求弹窗 -->
<div v-if="showAddModal" class="modal-overlay" @click="closeAddModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h2>添加需求</h2>
<button class="close-btn" @click="closeAddModal">&times;</button>
</div>
<div class="modal-body">
<form class="add-modal-form" @submit.prevent="submitAddForm">
<div class="form-row">
<span class="label">请求者</span>
<input v-model="addForm.requester" class="input" placeholder="可选" />
</div>
<div class="form-row">
<span class="label">QQ号</span>
<input v-model="addForm.qq_code" class="input" placeholder="可选" />
</div>
<div class="form-row">
<span class="label">需求内容</span>
<textarea
v-model="addForm.sendcontent"
class="input"
placeholder="请输入需求内容"
rows="3"
ref="autoTextarea"
@input="autoResize"
></textarea>
</div>
<div class="form-row">
<span class="label">赏金</span>
<input v-model="addForm.reward" class="input" placeholder="可选" />
</div>
<div v-if="addError" class="error">{{ addError }}</div>
<button class="btn-common btn-gradient submit-btn" :disabled="addLoading">
{{ addLoading ? '提交中...' : '提交' }}
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
// 响应式数据
const demands = ref([])
const loading = ref(false)
const error = ref(null)
const showModal = ref(false)
const selectedDemand = ref(null)
const showAddModal = ref(false)
const addError = ref('')
const addForm = ref({
requester: '',
sendcontent: '',
content: '',
reward: '',
qq_code: ''
})
const addLoading = ref(false)
const autoTextarea = ref(null)
// 判断是否为无赏金
const isNoReward = (reward) => {
return !reward || reward === '无赏金'
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString || dateString === 'Test_date') return '日期未提供'
try {
const date = new Date(dateString)
if (isNaN(date.getTime())) {
return dateString
}
const pad = num => num.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
} catch (e) {
return dateString
}
// 获取需求列表
const fetchDemands = async () => {
loading.value = true
error.value = null
try {
// 使用代理解决跨域问题
const targetUrl = 'http://49.232.8.189:8000/demands/getlist'
const response = await fetch(targetUrl, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`)
}
// 获取需求列表
const fetchDemands = async () => {
loading.value = true
error.value = null
try {
const response = await fetch('/api/demands/getlist', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
const data = await response.json()
demands.value = data
} catch (err) {
error.value = `加载失败: ${err.message}`
console.error('加载需求列表失败:', err)
} finally {
loading.value = false
})
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`)
}
const data = await response.json()
demands.value = data
} catch (err) {
error.value = `加载失败: ${err.message}`
console.error('加载需求列表失败:', err)
} finally {
loading.value = false
}
// 组件挂载时自动加载数据
onMounted(() => {
fetchDemands()
}
// 显示详情弹窗
const showDetail = (demand) => {
selectedDemand.value = demand
showModal.value = true
}
// 关闭弹窗
const closeModal = () => {
showModal.value = false
selectedDemand.value = null
}
// 打开弹窗
function openAddModal() {
addForm.value = {
requester: '',
sendcontent: '',
content: '',
reward: '',
qq_code: ''
}
addError.value = ''
showAddModal.value = true
}
// 关闭弹窗
function closeAddModal() {
showAddModal.value = false
addError.value = ''
}
//提交表单
async function submitAddForm() {
if (!addForm.value.sendcontent.trim()) {
addError.value = '需求内容不能为空';
return;
}
addLoading.value = true;
addError.value = '';
try {
const now = new Date();
const dateStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
const payload = {
requester: addForm.value.requester , // 如果requester为空则使用qq_code
sendcontent: addForm.value.sendcontent,
content: addForm.value.sendcontent,
reward: addForm.value.reward,
date: dateStr,
qq_code: addForm.value.qq_code
};
const response = await fetch('/api/demands/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
if (!response.ok) throw new Error('提交失败')
showAddModal.value = false
fetchDemands() // 刷新列表
} catch (e) {
addError.value = '提交失败,请稍后重试'
} finally {
addLoading.value = false
}
}
// 组件挂载时自动加载数据
onMounted(() => {
fetchDemands()
})
function autoResize() {
nextTick(() => {
const el = autoTextarea.value
if (el) {
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
}
})
</script>
<style scoped>
.demand-hall {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: #1890ff;
color: white;
padding: 20px;
border-radius: 5px 5px 0 0;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
text-align: center;
}
.app-content {
background-color: #f5f5f5;
padding: 20px;
border-radius: 0 0 5px 5px;
}
.demand-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.demand-card {
background-color: white;
border-radius: 5px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.demand-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.demand-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.requester {
font-weight: bold;
font-size: 18px;
color: #333;
}
.reward {
background-color: #fadb14;
color: #333;
padding: 3px 8px;
border-radius: 3px;
font-size: 14px;
}
.reward.no-reward {
background-color: #f5f5f5;
color: #999;
}
.content {
color: #666;
margin-bottom: 15px;
line-height: 1.6;
}
.date {
color: #999;
font-size: 12px;
text-align: right;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.error {
text-align: center;
padding: 20px;
color: #f5222d;
background-color: #fff1f0;
border-radius: 5px;
margin-bottom: 20px;
}
.refresh-btn {
display: block;
margin: 20px auto;
padding: 10px 20px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.refresh-btn:hover {
background-color: #40a9ff;
}
@media (max-width: 768px) {
.demand-list {
grid-template-columns: 1fr;
}
.demand-hall {
padding: 10px;
}
}
</style>
}
</script>
<style scoped>
.btn-common {
display: inline-block;
padding: 8px 22px;
font-size: 15px;
font-weight: 500;
border-radius: 6px;
border: 1px solid #b6d2ff;
cursor: pointer;
transition: background 0.2s, color 0.2s, border 0.2s;
outline: none;
box-shadow: none;
margin-bottom: 20px;
}
.btn-gradient {
background: linear-gradient(90deg, #71eaeb 0%, #416bdf 100%);
color: #fff;
border: 1px solid #71eaeb;
}
.btn-gradient:hover {
background: linear-gradient(90deg, #416bdf 0%, #71eaeb 100%);
color: #fff;
border: 1.5px solid #416bdf;
}
.btn-light {
background: linear-gradient(90deg, #e3f0ff 0%, #f7fbff 100%);
color: #2563eb;
border: 1px solid #b6d2ff;
}
.btn-light:hover {
background: linear-gradient(90deg, #d0e7ff 0%, #eaf4ff 100%);
color: #174ea6;
border: 1.5px solid #2563eb;
}
/* 按钮间距 */
.btn-margin-right {
margin-right: 16px;
}
.content {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 彻底重写 tag 样式,保证无阴影、紧凑、圆角 */
.tag {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
line-height: 1.2;
background: none;
box-shadow: none;
border: none;
margin: 0;
vertical-align: middle;
}
.has-reward {
background: #fff7e6;
color: #d46b08;
border: 1px solid #ffd591;
}
.no-reward {
background: #f5f5f5;
color: #8c8c8c;
border: 1px solid #d9d9d9;
}
.maps-table td.reward,
.maps-table tr {
box-shadow: none !important;
filter: none !important;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 80px;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
color: #1a237e;
font-size: 1.5rem;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #666;
cursor: pointer;
padding: 0;
line-height: 1;
}
.close-btn:hover {
color: #1a237e;
}
.modal-body {
padding: 20px;
}
.detail-item {
margin-bottom: 15px;
}
.detail-item:last-child {
margin-bottom: 0;
}
.label {
font-weight: 600;
color: #1a237e;
margin-right: 10px;
}
.value {
color: #333;
}
.maps-table tr {
height: 60px;
}
.maps-table th,
.maps-table td {
text-align: center;
vertical-align: middle !important;
padding: 12px 8px;
}
.maps-table td.reward {
text-align: center;
vertical-align: middle;
}
.input {
width: 70%;
padding: 6px 10px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
margin-left: 8px;
margin-top: 2px;
margin-bottom: 2px;
box-sizing: border-box;
}
.input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 2px #e3f0ff;
}
.error {
color: #f5222d;
background: #fff1f0;
border-radius: 4px;
padding: 8px 12px;
margin: 10px 0 0 0;
font-size: 14px;
text-align: center;
}
.add-modal-form {
display: flex;
flex-direction: column;
gap: 18px;
}
.add-modal-form .form-row {
display: flex;
align-items: flex-start;
gap: 10px;
}
.add-modal-form .label {
min-width: 70px;
font-weight: 600;
color: #1a237e;
margin-right: 0;
text-align: right;
padding-top: 6px;
}
.add-modal-form .input,
.add-modal-form textarea {
flex: 1;
width: 100%;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 5px;
font-size: 15px;
background: #f7faff;
transition: border 0.2s, box-shadow 0.2s;
resize: none;
}
.add-modal-form .input:focus,
.add-modal-form textarea:focus {
border-color: #2563eb;
box-shadow: 0 0 0 2px #e3f0ff;
outline: none;
}
.add-modal-form .error {
color: #f5222d;
background: #fff1f0;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
text-align: center;
margin: 0;
}
.add-modal-form .submit-btn {
width: 100%;
margin-top: 8px;
}
.add-modal-form textarea {
min-height: 60px;
max-height: 200px;
resize: vertical;
line-height: 1.6;
overflow-y: auto;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="maps">
<div class="page-header">
<h1>热门下载地图</h1>
<h1>最近上传地图</h1>
</div>
<div class="filters">
<div class="search-box">
@@ -115,7 +115,6 @@ import '../assets/styles/common.css'
import '../assets/styles/Maps.css'
const router = useRouter()
const route = useRoute()
const maps = ref([])
const currentPage = ref(1)
const totalPages = ref(1)
@@ -124,14 +123,14 @@ const jumpPage = ref('')
// 排序和筛选相关
const orderOptions = [
{ label: '创建时间降序', value: '-create_time' },
{ label: '创建时间升序', value: 'create_time' },
{ label: '下载量降序', value: '-download_count' },
{ label: '下载量升序', value: 'download_count' },
{ label: '收藏数降序', value: '-favourite_count' },
{ label: '收藏数升序', value: 'favourite_count' },
{ label: '创建时间降序', value: '-create_time' },
{ label: '创建时间升序', value: 'create_time' }
{ label: '收藏数升序', value: 'favourite_count' }
]
const selectedOrder = ref('-download_count')
const selectedOrder = ref('-create_time')
const searchValue = ref('')
const playerCountFilter = ref('')
const tagFilter = ref('')
@@ -170,9 +169,40 @@ const fetchMaps = async (page = 1) => {
tags: tagFilter.value,
ordering: selectedOrder.value
}
console.log('正在获取地图数据,参数:', params)
const data = await getMaps(params)
maps.value = data.results
// 只在创建时间排序时进行日期分组和下载量排序
if (selectedOrder.value === '-create_time' || selectedOrder.value === 'create_time') {
// 按日期分组并排序
const groupedMaps = data.results.reduce((groups, map) => {
const date = new Date(map.create_time).toLocaleDateString('zh-CN')
if (!groups[date]) {
groups[date] = []
}
groups[date].push(map)
return groups
}, {})
// 对每个日期组内的地图按下载量降序排序
Object.keys(groupedMaps).forEach(date => {
groupedMaps[date].sort((a, b) => b.download_count - a.download_count)
})
// 获取所有日期并排序
const sortedDates = Object.keys(groupedMaps).sort((a, b) => {
return selectedOrder.value === '-create_time'
? new Date(b) - new Date(a) // 降序
: new Date(a) - new Date(b) // 升序
})
// 按日期顺序拼接所有地图
maps.value = sortedDates.reduce((acc, date) => {
return acc.concat(groupedMaps[date])
}, [])
} else {
maps.value = data.results
}
hasNextPage.value = !!data.next
totalPages.value = Math.ceil(data.count / 20)
currentPage.value = page

View File

@@ -1,7 +1,7 @@
<template>
<div class="weekly-recommend">
<div class="page-header">
<h1>每周地图推荐</h1>
<h1>热门下载地图</h1>
<div class="header-subtitle">
<span class="date-range">{{ currentWeekRange }}</span>
</div>