DCFronted/src/components/backend/ServiceHallView.vue
2025-05-28 18:36:11 +08:00

526 lines
16 KiB
Vue

<template>
<div class="service-hall-container">
<div class="actions-bar">
<h3>办事大厅</h3>
<div class="buttons-wrapper">
<button @click="showAddDemandModal = true" class="add-button">添加需求</button>
<button @click="fetchDemandsAdmin" class="refresh-button">刷新列表</button>
</div>
</div>
<div class="table-responsive-wrapper">
<table class="custom-table">
<thead>
<tr>
<th>ID</th>
<th>请求者</th>
<th>QQ号</th>
<th>请求内容</th>
<th>悬赏金额</th>
<th>创建日期</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td :colspan="7" style="text-align: center;">加载中...</td>
</tr>
<tr v-else-if="demands.length === 0">
<td :colspan="7" style="text-align: center;">暂无需求数据</td>
</tr>
<tr v-for="demand in demands" :key="demand.id">
<td>{{ demand.id }}</td>
<td>{{ demand.requester || '-' }}</td>
<td>{{ demand.qq_code || '-' }}</td>
<td class="content-cell" :title="demand.content">{{ truncateText(demand.content, 50) }}</td>
<td>{{ demand.reward || '无赏金' }}</td>
<td>{{ formatDate(demand.date) }}</td>
<td>
<button @click="editDemand(demand)" class="action-button edit">编辑</button>
<button @click="confirmDeleteDemand(demand.id)" class="action-button delete">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 编辑需求模态框 -->
<div v-if="showEditDemandModal" class="modal-overlay" @click.self="closeEditModal">
<div class="modal-content">
<h2>编辑需求</h2>
<form @submit.prevent="submitEditForm">
<div class="form-group">
<label :for="`edit-requester-${currentDemand.id}`">请求者:</label>
<input :id="`edit-requester-${currentDemand.id}`" type="text" v-model="editForm.requester">
</div>
<div class="form-group">
<label :for="`edit-qq-${currentDemand.id}`">QQ号:</label>
<input :id="`edit-qq-${currentDemand.id}`" type="text" v-model="editForm.qq_code">
</div>
<div class="form-group">
<label :for="`edit-content-${currentDemand.id}`">需求内容:</label>
<textarea :id="`edit-content-${currentDemand.id}`" v-model="editForm.sendcontent" rows="4" required></textarea>
</div>
<div class="form-group">
<label :for="`edit-reward-${currentDemand.id}`">悬赏金额:</label>
<input :id="`edit-reward-${currentDemand.id}`" type="text" v-model="editForm.reward">
</div>
<div class="form-group">
<label :for="`edit-date-${currentDemand.id}`">日期 (YYYY-MM-DD HH:MM:SS):</label>
<input :id="`edit-date-${currentDemand.id}`" type="text" v-model="editForm.date" readonly>
</div>
<div class="form-actions">
<button type="submit" class="submit-button">更新</button>
<button type="button" @click="closeEditModal" class="cancel-button">取消</button>
</div>
<p v-if="editError" class="error-message">{{ editError }}</p>
</form>
</div>
</div>
<!-- 添加需求模态框 -->
<div v-if="showAddDemandModal" class="modal-overlay" @click.self="closeAddModal">
<div class="modal-content">
<h2>添加新需求</h2>
<form @submit.prevent="submitAddForm">
<div class="form-group">
<label for="add-requester">请求者 (可选):</label>
<input id="add-requester" type="text" v-model="addForm.requester">
</div>
<div class="form-group">
<label for="add-qq">QQ号 (可选):</label>
<input id="add-qq" type="text" v-model="addForm.qq_code">
</div>
<div class="form-group">
<label for="add-content">需求内容:</label>
<textarea id="add-content" v-model="addForm.sendcontent" rows="4" required></textarea>
</div>
<div class="form-group">
<label for="add-reward">悬赏金额 (可选):</label>
<input id="add-reward" type="text" v-model="addForm.reward">
</div>
<div class="form-actions">
<button type="submit" class="submit-button" :disabled="addLoading">{{ addLoading ? '提交中...' : '提交' }}</button>
<button type="button" @click="closeAddModal" class="cancel-button">取消</button>
</div>
<p v-if="addError" class="error-message">{{ addError }}</p>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getDemandsList, updateDemand, deleteDemand, addDemand } from '../../api/demands'; // 确认路径
const demands = ref([]);
const loading = ref(false);
const showEditDemandModal = ref(false);
const currentDemand = ref(null);
const editForm = ref({});
const editError = ref('');
// Refs for Add Demand Modal
const showAddDemandModal = ref(false);
const addLoading = ref(false);
const initialAddFormState = {
requester: '',
qq_code: '',
sendcontent: '',
reward: ''
// date is generated on submit
};
const addForm = ref({ ...initialAddFormState });
const addError = ref('');
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return dateString; // 如果日期无效,返回原始字符串
return date.toLocaleString(); // 或者更复杂的格式化
} catch (e) {
return dateString;
}
};
const truncateText = (text, length) => {
if (!text) return '';
return text.length > length ? text.substring(0, length) + '...' : text;
};
const fetchDemandsAdmin = async () => {
loading.value = true;
try {
const response = await getDemandsList();
demands.value = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('获取需求列表失败 (admin):', error);
demands.value = [];
} finally {
loading.value = false;
}
};
const editDemand = (demand) => {
currentDemand.value = { ...demand };
// 确保 editForm 使用 demand 中的原始 date 字符串,而不是格式化后的
editForm.value = {
...demand,
sendcontent: demand.content || demand.sendcontent // API中content和sendcontent可能混用
};
editError.value = '';
showEditDemandModal.value = true;
};
const closeEditModal = () => {
showEditDemandModal.value = false;
currentDemand.value = null;
editError.value = '';
};
const submitEditForm = async () => {
if (!currentDemand.value || !currentDemand.value.id) return;
editError.value = '';
try {
// 构造符合 updateDemand API 的 payload
const payload = {
requester: editForm.value.requester,
qq_code: editForm.value.qq_code,
sendcontent: editForm.value.sendcontent,
reward: editForm.value.reward,
date: editForm.value.date // 使用表单中的日期
// content 会在API层通过 sendcontent 补齐
};
await updateDemand(currentDemand.value.id, payload);
alert('需求更新成功!');
closeEditModal();
fetchDemandsAdmin();
} catch (error) {
console.error('更新需求失败:', error);
editError.value = error.response?.data?.detail || error.message || '更新失败,请重试。';
}
};
const confirmDeleteDemand = async (id) => {
if (window.confirm('确定要删除此需求吗?')) {
try {
await deleteDemand(id);
alert('需求删除成功!');
fetchDemandsAdmin();
} catch (error) {
console.error('删除需求失败:', error);
alert('删除需求失败: ' + (error.message || '请稍后重试'));
}
}
};
// --- Add Demand Modal Logic ---
const openAddModal = () => {
addForm.value = { ...initialAddFormState };
addError.value = '';
showAddDemandModal.value = true;
};
const closeAddModal = () => {
showAddDemandModal.value = false;
addError.value = '';
};
const submitAddForm = async () => {
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 = {
...addForm.value,
date: dateStr,
// content will be handled by addDemand API function if sendcontent is provided
};
await addDemand(payload);
alert('新需求添加成功!');
closeAddModal();
fetchDemandsAdmin(); // Refresh list
} catch (error) {
console.error('添加需求失败:', error);
addError.value = error.response?.data?.detail || error.message || '添加失败,请稍后重试。';
} finally {
addLoading.value = false;
}
};
onMounted(() => {
fetchDemandsAdmin();
});
</script>
<style scoped>
.service-hall-container {
padding: 15px; /* Reduced padding for mobile */
}
.actions-bar {
margin-bottom: 20px;
display: flex;
flex-wrap: wrap; /* Allow wrapping */
justify-content: space-between;
align-items: center;
gap: 10px; /* Add gap for wrapped items */
}
.actions-bar h3 {
margin: 0;
font-size: 1.4rem; /* Slightly reduced for mobile */
color: #333;
}
/* Container for buttons on the right */
.actions-bar .buttons-wrapper {
display: flex;
gap: 10px; /* Space between buttons */
}
.refresh-button, .add-button {
background-color: #1890ff;
color: white;
border: none;
padding: 8px 12px; /* Slightly reduced padding */
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem; /* Slightly reduced font size */
}
.refresh-button:hover, .add-button:hover {
background-color: #40a9ff;
}
.table-responsive-wrapper { /* New wrapper for table */
width: 100%;
overflow-x: auto; /* Enable horizontal scrolling */
-webkit-overflow-scrolling: touch; /* Smoother scrolling on iOS */
}
.custom-table {
width: 100%;
min-width: 750px; /* Minimum width before scrolling starts, adjust as needed */
border-collapse: collapse;
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.12); /* Softer shadow */
}
.custom-table th, .custom-table td {
border: 1px solid #e8e8e8;
padding: 10px 12px; /* Adjusted padding */
text-align: left;
font-size: 0.85rem; /* Adjusted font size */
vertical-align: middle;
/* white-space: nowrap; /* Consider if content should wrap or not. Ellipsis is handled by truncateText */
}
.custom-table th {
background-color: #fafafa;
font-weight: 600;
color: #000000d9;
white-space: nowrap; /* Keep headers on one line */
}
.custom-table tbody tr:hover {
background-color: #f5f5f5;
}
.content-cell {
max-width: 250px; /* Adjust as needed */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.action-button {
padding: 5px 8px; /* Adjusted padding */
border-radius: 3px; /* Adjusted border-radius */
border: none;
cursor: pointer;
font-size: 0.8rem; /* Adjusted font size */
margin-right: 5px;
color: white;
white-space: nowrap; /* Keep button text on one line */
}
.action-button.edit {
background-color: #52c41a;
}
.action-button.edit:hover {
background-color: #73d13d;
}
.action-button.delete {
background-color: #ff4d4f;
}
.action-button.delete:hover {
background-color: #ff7875;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 10px; /* Add padding for very small screens */
}
.modal-content {
background-color: white;
padding: 20px; /* Adjusted padding */
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
width: 90%;
max-width: 550px;
z-index: 1001;
}
.modal-content h2 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
font-size: 1.3rem; /* Adjusted font size */
}
.form-group {
margin-bottom: 12px; /* Adjusted margin */
}
.form-group label {
display: block;
margin-bottom: 4px;
font-size: 0.85rem; /* Adjusted font size */
font-weight: 500;
color: #555;
}
.form-group input[type="text"],
.form-group textarea {
padding: 8px 10px; /* Adjusted padding */
font-size: 0.85rem; /* Adjusted font size */
width: 100%; /* Ensure form elements take full width */
box-sizing: border-box; /* Include padding and border in the element's total width and height */
border: 1px solid #ccc;
border-radius: 4px;
}
.form-group input[type="text"]:focus,
.form-group textarea:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-actions {
margin-top: 20px; /* Adjusted margin */
}
.submit-button, .cancel-button {
padding: 8px 15px; /* Adjusted padding */
font-size: 0.85rem; /* Adjusted font size */
border-radius: 4px;
border: none;
cursor: pointer;
}
.submit-button {
background-color: #1890ff;
color: white;
margin-right: 10px;
}
.submit-button:hover {
background-color: #40a9ff;
}
.cancel-button {
background-color: #f0f0f0;
color: #333;
}
.cancel-button:hover {
background-color: #e0e0e0;
}
.error-message {
color: red;
margin-top: 10px;
font-size: 0.85rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.actions-bar {
flex-direction: column; /* Stack title and buttons vertically */
align-items: flex-start; /* Align items to the start */
}
.actions-bar h3 {
margin-bottom: 10px; /* Add space below title when stacked */
}
.actions-bar .buttons-wrapper { /* Container for buttons */
width: 100%; /* Full width on mobile */
/* justify-content: space-between; /* Remove this if you don't want them to spread far apart */
/* Consider justify-content: flex-start; or let gap handle spacing */
}
.refresh-button, .add-button {
/* flex-grow: 1; /* Remove flex-grow if buttons should take natural width */
/* margin: 0 5px; /* Remove or adjust margin, gap is now on buttons-wrapper */
/* width: auto; /* Ensure buttons take their content width or a defined width */
padding: 8px 15px; /* Ensure decent padding */
}
/* If you want them to still be somewhat spread on mobile but not full flex-grow:
.actions-bar .buttons-wrapper {
display: flex;
justify-content: flex-end; /* Aligns buttons to the right on mobile when stacked
}
.refresh-button, .add-button {
margin-left: 10px;
}
.refresh-button:first-child, .add-button:first-child { margin-left: 0; }
*/
}
@media (max-width: 480px) {
.modal-content {
padding: 15px;
}
.modal-content h2 {
font-size: 1.2rem;
}
.actions-bar .buttons-wrapper button { /* Target buttons within the wrapper more specifically if needed */
padding: 8px 10px; /* Even smaller padding for very small buttons */
font-size: 0.8rem;
/* width: 100%; /* Optionally make buttons full width on very small screens */
/* margin-bottom: 5px; /* If they stack vertically */
}
/* If buttons stack vertically on very small screens (example):
.actions-bar .buttons-wrapper {
flex-direction: column;
align-items: stretch;
}
.actions-bar .buttons-wrapper button {
width: 100%;
margin-left: 0;
margin-bottom: 8px;
}
.actions-bar .buttons-wrapper button:last-child {
margin-bottom: 0;
}
*/
.content-cell {
max-width: 100px; /* Further reduce for very small screens */
}
}
</style>