DCFronted/src/views/index/DemandList.vue
2025-06-18 20:40:29 +08:00

831 lines
20 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.

<template>
<div class="demand-hall">
<div class="page-header">
<h1>需求列表</h1>
<h3>该功能仅做预约联系使用不设计现实中的货币账户一般等价物请用户分辨明细防止电信诈骗</h3>
</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>
<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>
</td>
<td class="date">{{ formatDate(demand.date) }}</td>
<td>
<button class="btn-common btn-gradient btn-reply" @click.stop="showReply(demand)">回复</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 回复查看表单,联系方式,昵称,相关建议-->
<div v-if="replyModal" class="modal-overlay" @click="closeReplyModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h2>回复</h2>
<button class="close-btn" @click="closeReplyModal">&times;</button>
</div>
<div class="modal-body">
<form class="add-modal-form" @submit.prevent="submitReply">
<div class="form-row">
<span class="label">昵称:</span>
<input
v-model="addForm.author"
class="input"
placeholder="请输入您的昵称"
required
/>
</div>
<div class="form-row">
<span class="label">QQ号</span>
<input
v-model="addForm.author_contact"
class="input"
placeholder="请输入您的QQ号"
required
/>
</div>
<div class="form-row">
<span class="label">相关建议:</span>
<textarea
v-model="addForm.sendcontent"
class="input"
placeholder="请输入相关建议"
rows="3"
ref="autoTextarea"
@input="autoResize"
required
></textarea>
</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 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 class="reply-section">
<h3>回复内容</h3>
<div class="reply-list">
<div v-if="selectedDemand?.replies && selectedDemand.replies.length > 0">
<div v-for="reply in selectedDemand.replies" :key="reply.id" class="reply-item">
<div class="reply-header">
<span class="reply-author">{{ reply.author || '匿名用户' }}</span>
<span class="reply-time">{{ formatDate(reply.time) }}</span>
</div>
<div class="reply-content">{{ reply.content }}</div>
</div>
</div>
<div v-else class="no-reply">
<div v-if="selectedDemand?.sendcontent">
<div v-for="(reply, index) in selectedDemand.sendcontent.split('|')" :key="index" class="reply-content">
<div class="reply-with-avatar">
<img :src="`https://q1.qlogo.cn/g?b=qq&nk=${reply.match(/\((\d+)\)/)?.[1] || ''}&s=40`" alt="User Avatar" class="reply-avatar" />
<div class="reply-text">
<b>{{ reply.split(')')[0] + '' }}</b>{{ reply.split(')')[1] }}
</div>
</div>
</div>
</div>
<div v-else>
暂无回复
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加需求弹窗 -->
<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">
需求一经发布不许修改
</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="可选"
@input="validateQQ"
type="text"
pattern="[0-9]*"
inputmode="numeric"
/>
</div>
<div class="form-row">
<span class="label">需求内容:</span>
<textarea
v-model="addForm.content"
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'
import { getDemandsList, addDemand, updateDemand } from '../../api/demands'
// 响应式数据
const demands = ref([])
const loading = ref(false)
const error = ref(null)
const showModal = ref(false)
const reply = ref(null)
const replyModal = ref(false)
const selectedDemand = ref(null)
const showAddModal = ref(false)
const addError = ref('')
const addForm = ref({
requester: '',
content: '',
reward: '',
qq_code: '',
sendcontent: '',
author: '',
author_contact: ''
})
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 data = await getDemandsList()
demands.value = data
} catch (err) {
error.value = `加载失败: ${err.message}`
console.error('加载需求列表失败:', err)
} finally {
loading.value = false
}
}
//回复显示弹窗
const showReply = (demand) => {
reply.value = demand
replyModal.value = true
resetReplyForm();
}
// 关闭回复弹窗
const closeReplyModal = () => {
replyModal.value = false;
resetReplyForm();
}
// 重置回复表单
const resetReplyForm = () => {
addForm.value = {
sendcontent: '',
author: '',
author_contact: ''
};
addError.value = '';
// 重置文本框高度
if (autoTextarea.value) {
autoTextarea.value.style.height = 'auto';
}
}
// 显示详情弹窗
const showDetail = (demand) => {
selectedDemand.value = demand
showModal.value = true
}
// 关闭弹窗
const closeModal = () => {
showModal.value = false
selectedDemand.value = null
}
// 打开弹窗
const openAddModal = () => {
// 重置表单数据
addForm.value = {
requester: '',
content: '',
reward: '',
qq_code: '',
sendcontent: '',
author: '',
author_contact: ''
};
addError.value = '';
showAddModal.value = true;
}
// 关闭弹窗
function closeAddModal() {
showAddModal.value = false
addError.value = ''
}
// 在 script setup 部分添加验证函数
const validateQQ = (event) => {
// 只保留数字
addForm.value.qq_code = event.target.value.replace(/[^\d]/g, '');
}
// 修改 submitAddForm 函数,添加 QQ 号验证
async function submitAddForm() {
if (!addForm.value.content?.trim()) {
addError.value = '需求内容不能为空';
return;
}
// 如果填写了QQ号验证是否为纯数字
if (addForm.value.qq_code && !/^\d+$/.test(addForm.value.qq_code)) {
addError.value = 'QQ号必须为纯数字';
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 || '',
sendcontent: '',
content: addForm.value.content,
reward: addForm.value.reward || '',
date: dateStr,
qq_code: addForm.value.qq_code || ''
};
console.log('提交的数据:', payload);
await addDemand(payload);
showAddModal.value = false;
fetchDemands(); // 刷新列表
} catch (e) {
console.error('提交失败:', e);
addError.value = e.response?.data?.detail || '提交失败,请稍后重试';
} finally {
addLoading.value = false;
}
}
// 提交回复
const submitReply = async () => {
if (!addForm.value.sendcontent?.trim()) {
addError.value = '回复内容不能为空';
return;
}
if (!addForm.value.author?.trim()) {
addError.value = '昵称不能为空';
return;
}
if (!addForm.value.author_contact?.trim()) {
addError.value = 'QQ号不能为空';
return;
}
// 验证QQ号是否为纯数字
if (!/^\d+$/.test(addForm.value.author_contact)) {
addError.value = 'QQ号必须为纯数字';
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 newReply = `${addForm.value.author}(${addForm.value.author_contact})${addForm.value.sendcontent}`;
const formattedContent = reply.value.sendcontent
? `${reply.value.sendcontent}|${newReply}`
: newReply;
const payload = {
requester: reply.value.requester || '',
sendcontent: formattedContent,
content: reply.value.content,
reward: reply.value.reward || '',
date: dateStr,
qq_code: reply.value.qq_code || '',
author: addForm.value.author,
author_contact: addForm.value.author_contact
};
console.log('提交的回复数据:', payload);
await updateDemand(reply.value.id, payload);
replyModal.value = false;
resetReplyForm();
fetchDemands(); // 刷新列表
} catch (e) {
console.error('提交回复失败:', e);
addError.value = e.response?.data?.detail || '提交失败,请稍后重试';
} 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>
.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;
}
.btn-reply {
padding: 5px 15px;
font-size: 14px;
margin-bottom: 0;
}
.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: 800px;
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;
}
.reply-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.reply-section h3 {
color: #1a237e;
margin-bottom: 15px;
font-size: 1.2rem;
}
.reply-list {
background: none;
border-radius: 0;
padding: 0 0 0 0;
}
.reply-item {
background: none;
border-radius: 0;
padding: 0 0 0 0;
}
.reply-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.reply-author {
font-weight: 600;
color: #1a237e;
}
.reply-time {
color: #666;
font-size: 0.9em;
}
.reply-content {
color: #333;
line-height: 1.5;
}
.no-reply {
text-align: center;
color: #666;
padding: 20px;
background: #f7faff;
border-radius: 8px;
}
.no-reply .reply-content {
text-align: left;
padding: 10px 0;
color: #333;
border-left: 3px solid #2563eb;
padding-left: 15px;
margin: 10px 0;
font-size: 14px;
line-height: 1.6;
background: transparent;
box-shadow: none;
}
.reply-with-avatar {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 16px;
}
.reply-with-avatar:last-child {
margin-bottom: 0;
}
.reply-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
object-fit: cover;
flex-shrink: 0;
background: #f3f6fa;
}
.reply-text {
flex: 1;
display: flex;
align-items: center;
min-height: 40px;
font-size: 15px;
color: #222;
line-height: 1.7;
padding-left: 14px;
background: none;
border-radius: 0;
box-shadow: none;
}
.reply-text b {
font-weight: 600;
margin-right: 6px;
color: #2563eb;
font-size: 15px;
}
</style>