移动端适配

This commit is contained in:
Kunagisa 2025-06-29 18:06:37 +08:00
parent afc39cc066
commit 0e73e24b4d
4 changed files with 157 additions and 68 deletions

View File

@ -75,4 +75,27 @@ export const deleteDemand = async (id) => {
console.error('删除需求失败:', error); console.error('删除需求失败:', error);
throw error; throw error;
} }
}; };
/**
* 添加需求回复
* @param {number} id - 需求ID
* @param {Object} replyData - 回复数据
* @param {string} replyData.reply - 回复内容
* @returns {Promise<Object>} 返回添加回复的响应数据
* 说明会将回复内容与用户qq拼接在一起格式qq:内容|qq:内容
*/
export const addDemandReply = async (id, replyData) => {
try {
const payload = {
id: id,
reply: replyData.reply
};
console.log('添加需求回复的数据:', payload);
const response = await axiosInstance.put('/demands/reply', payload);
return response.data;
} catch (error) {
console.error('添加需求回复失败:', error);
throw error;
}
};

View File

@ -69,6 +69,7 @@
</div> </div>
<div class="admin-main-content"> <div class="admin-main-content">
<AdminEditUserPrivilege v-if="currentAdminView === 'admin-edit-user-privilege'" /> <AdminEditUserPrivilege v-if="currentAdminView === 'admin-edit-user-privilege'" />
<AffairManagement v-if="currentAdminView === 'affair-management'" />
</div> </div>
</div> </div>
</div> </div>
@ -79,6 +80,7 @@ import { ref, onMounted, computed, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getUserInfo } from '@/utils/jwt' import { getUserInfo } from '@/utils/jwt'
import AdminEditUserPrivilege from '@/components/backend/AdminEditUserPrivilege.vue' import AdminEditUserPrivilege from '@/components/backend/AdminEditUserPrivilege.vue'
import AffairManagement from '@/components/backend/AffairManagement.vue'
const router = useRouter() const router = useRouter()
const hasToken = ref(false) const hasToken = ref(false)
@ -91,7 +93,11 @@ const dropdownOpen3 = ref(false)
let privilegeCheckTimer = null let privilegeCheckTimer = null
const isAdmin = computed(() => { const isAdmin = computed(() => {
return currentUserData.value && currentUserData.value.privilege === 'lv-admin'; return currentUserData.value && (
currentUserData.value.privilege === 'lv-admin' ||
currentUserData.value.privilege === 'lv-user' ||
currentUserData.value.privilege === 'admin'
);
}) })
async function checkPrivilege() { async function checkPrivilege() {
@ -106,7 +112,7 @@ async function checkPrivilege() {
try { try {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
currentUserData.value = userInfo; currentUserData.value = userInfo;
if (!userInfo || userInfo.privilege !== 'lv-admin') { if (!userInfo || (userInfo.privilege !== 'lv-admin' && userInfo.privilege !== 'lv-user' && userInfo.privilege !== 'admin')) {
// 退 // 退
localStorage.removeItem('access_token') localStorage.removeItem('access_token')
currentUserData.value = null currentUserData.value = null

View File

@ -602,4 +602,84 @@ const showCompetitionMenu = computed(() => showCompetition.value)
background-color: #f5f5f5; background-color: #f5f5f5;
color: #416bdf; color: #416bdf;
} }
@media (max-width: 768px) {
/* 整体导航栏布局 */
.nav-container {
flex-direction: column;
align-items: stretch;
}
.nav-brand {
text-align: center;
margin-right: 0;
}
/* 固定汉堡菜单按钮到右上角 */
.mobile-menu-toggle {
position: fixed;
top: 10px;
right: 12px;
z-index: 1002;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 50%;
padding: 8px;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
/* 移动端菜单展开后的容器样式 */
.nav-left.active,
.nav-right.active {
display: flex !important; /* 由脚本控制,我们只定义样式 */
flex-direction: column;
width: 100%;
gap: 5px;
padding: 10px 0;
}
.nav-right {
margin-left: 0;
}
/* 调整下拉菜单在一级菜单内的定位方式 */
.nav-dropdown {
position: static;
}
/* 下拉菜单内容区的移动端样式 */
.dropdown-content {
position: static; /* 覆盖桌面端的 absolute 定位 */
background-color: transparent;
box-shadow: none;
border-radius: 0;
padding: 8px 0 8px 15px; /* 子菜单缩进 */
margin-top: 5px;
border-left: 2px solid rgba(255, 255, 255, 0.15);
min-width: unset;
width: 100%;
}
/* 下拉菜单中的链接在移动端的样式 */
.dropdown-content .nav-link {
color: rgba(255, 255, 255, 0.9);
font-weight: normal;
}
.dropdown-content .nav-link:hover {
color: #fff;
background-color: rgba(255, 255, 255, 0.1);
}
/* 用户登录/信息区域的移动端样式 */
.login-btn,
.user-info-nav {
width: 100%;
justify-content: center;
margin: 5px 0;
}
}
</style> </style>

View File

@ -22,7 +22,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(demand, index) in demands" :key="demand.id" class="table-row" @click="showDetail(demand)"> <tr v-for="(demand, index) in filteredDemands" :key="demand.id" class="table-row" @click="showDetail(demand)">
<td class="id">{{ index + 1 }}</td> <td class="id">{{ index + 1 }}</td>
<td class="requester"> <td class="requester">
<span v-if="!demand.requester" class="tag no-reward">匿名</span> <span v-if="!demand.requester" class="tag no-reward">匿名</span>
@ -55,13 +55,6 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="add-modal-form" @submit.prevent="submitReply"> <form class="add-modal-form" @submit.prevent="submitReply">
<!-- <div class="form-row">-->
<!-- <span class="label">回复消息</span>-->
<!-- <select v-model="addForm.replyTo" class="input">-->
<!-- <option value=""></option>-->
<!-- <option v-for="msg in replyOptions" :key="msg.id" :value="msg.id">{{ msg.text }}</option>-->
<!-- </select>-->
<!-- </div>-->
<div class="form-row"> <div class="form-row">
<span class="label">昵称</span> <span class="label">昵称</span>
<input <input
@ -132,7 +125,7 @@
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="label">需求内容</span> <span class="label">需求内容</span>
<span class="value">{{ selectedDemand?.content }}</span> <span class="value">{{ selectedDemand?.content?.replace(/^&DEL/, '') || selectedDemand?.content }}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="label">赏金</span> <span class="label">赏金</span>
@ -263,11 +256,11 @@
<div v-if="showDeleteConfirm" class="modal-overlay" style="z-index:2000;"> <div v-if="showDeleteConfirm" class="modal-overlay" style="z-index:2000;">
<div class="modal-content" style="max-width:350px;text-align:center;"> <div class="modal-content" style="max-width:350px;text-align:center;">
<div class="modal-header"> <div class="modal-header">
<h2 style="color:#F56C6C;">删除确认</h2> <h2 style="color:#F56C6C;">隐藏并删除</h2>
</div> </div>
<div class="modal-body" style="font-size:16px;">确定要删除该需求吗此操作不可恢复</div> <div class="modal-body" style="font-size:16px;">确定要隐藏并删除该需求吗此操作不可恢复</div>
<div class="delete-dialog-footer" style="display:flex;justify-content:center;gap:18px;margin:18px 0 8px 0;"> <div class="delete-dialog-footer" style="display:flex;justify-content:center;gap:18px;margin:18px 0 8px 0;">
<button class="confirm-button" @click="confirmDelete">确认删除</button> <button class="confirm-button" @click="confirmDelete">确认</button>
<button class="cancel-button" @click="cancelDelete">取消</button> <button class="cancel-button" @click="cancelDelete">取消</button>
</div> </div>
</div> </div>
@ -278,7 +271,7 @@
<script setup> <script setup>
import { ref, onMounted, nextTick, computed } from 'vue' import { ref, onMounted, nextTick, computed } from 'vue'
import { getDemandsList, addDemand, updateDemand, deleteDemand } from '../../api/demands' import { getDemandsList, addDemand, updateDemand, deleteDemand, addDemandReply } from '../../api/demands'
import { getStoredUser } from '@/utils/jwt' import { getStoredUser } from '@/utils/jwt'
import ErrorDialog from '@/components/ErrorDialog.vue' import ErrorDialog from '@/components/ErrorDialog.vue'
@ -322,6 +315,12 @@ const pendingDeleteId = ref(null)
const isNoReward = (reward) => { const isNoReward = (reward) => {
return !reward || reward === '无赏金' return !reward || reward === '无赏金'
} }
// &DEL
const filteredDemands = computed(() => {
return demands.value.filter(demand => !demand.content?.startsWith('&DEL'))
})
// //
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString || dateString === 'Test_date') return '日期未提供' if (!dateString || dateString === 'Test_date') return '日期未提供'
@ -363,24 +362,6 @@ const fetchDemands = async () => {
const showReply = (demand) => { const showReply = (demand) => {
reply.value = demand reply.value = demand
replyModal.value = true replyModal.value = true
// sendcontent
if (demand.sendcontent) {
replyOptions.value = demand.sendcontent.split('|').map((msg, idx) => {
// qq
// qq (qq)
let match = msg.match(/^(.+?)[(](\d+)[)]/)
let nickname = match ? match[1] : ''
let qq = match ? match[2] : ''
return {
id: idx,
text: msg,
nickname,
qq
}
})
} else {
replyOptions.value = []
}
resetReplyForm(); resetReplyForm();
} }
@ -397,8 +378,7 @@ const resetReplyForm = () => {
addForm.value = { addForm.value = {
sendcontent: '', sendcontent: '',
author: '', author: '',
author_contact: user && user.qq_code ? user.qq_code : '', author_contact: user && user.qq_code ? user.qq_code : ''
replyTo: ''
}; };
addError.value = ''; addError.value = '';
// //
@ -430,8 +410,7 @@ const openAddModal = () => {
qq_code: user && user.qq_code ? user.qq_code : '', qq_code: user && user.qq_code ? user.qq_code : '',
sendcontent: '', sendcontent: '',
author: '', author: '',
author_contact: '', author_contact: ''
replyTo: ''
}; };
addError.value = ''; addError.value = '';
showAddModal.value = true; showAddModal.value = true;
@ -506,36 +485,12 @@ const submitReply = async () => {
addLoading.value = true; addLoading.value = true;
addError.value = ''; addError.value = '';
try { try {
const now = new Date(); const replyData = {
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')}`; reply: addForm.value.sendcontent
//
let newReply = '';
if (addForm.value.replyTo !== '' && replyOptions.value.length > 0) {
const targetMsg = replyOptions.value.find(msg => String(msg.id) === String(addForm.value.replyTo));
let targetName = targetMsg && targetMsg.nickname ? targetMsg.nickname : '';
newReply = `${addForm.value.author}${addForm.value.author_contact})回复${targetName ? ' ' + targetName : ''}${addForm.value.sendcontent}`;
} else {
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); console.log('提交的回复数据:', replyData);
await updateDemand(reply.value.id, payload); await addDemandReply(reply.value.id, replyData);
replyModal.value = false; replyModal.value = false;
resetReplyForm(); resetReplyForm();
fetchDemands(); // fetchDemands(); //
@ -563,12 +518,28 @@ function autoResize() {
} }
function extractQQ(str) { function extractQQ(str) {
// qq:
const qqMatch = str.match(/^(\d+):(.+)$/)
if (qqMatch) {
return qqMatch[1]
}
// QQ
const match = str.match(/[(]([1-9][0-9]{4,})[)]/) const match = str.match(/[(]([1-9][0-9]{4,})[)]/)
return match ? match[1] : '' return match ? match[1] : ''
} }
function parseReplyText(str) { function parseReplyText(str) {
// / // qq:
const qqMatch = str.match(/^(\d+):(.+)$/)
if (qqMatch) {
return {
user: qqMatch[1],
content: qqMatch[2].trim()
}
}
// QQ
const match = str.match(/^(.+?[(][1-9][0-9]{4,}[)])(.*)$/) const match = str.match(/^(.+?[(][1-9][0-9]{4,}[)])(.*)$/)
if (match) { if (match) {
// match[1] QQmatch[2] " xxx " // match[1] QQmatch[2] " xxx "
@ -634,7 +605,16 @@ function handleDeleteDemand() {
async function confirmDelete() { async function confirmDelete() {
try { try {
await deleteDemand(pendingDeleteId.value) // 使updateDemand APIcontent&DEL
const updatedContent = `&DEL${selectedDemand.value.content}`
await updateDemand(pendingDeleteId.value, {
requester: selectedDemand.value.requester,
qq_code: selectedDemand.value.qq_code,
content: updatedContent,
reward: selectedDemand.value.reward,
date: selectedDemand.value.date,
sendcontent: selectedDemand.value.sendcontent
})
fetchDemands() fetchDemands()
closeModal() closeModal()
} catch (e) { } catch (e) {