526 lines
16 KiB
Vue
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> |