feat: 反馈处理增加回复功能 — 管理员可回复用户反馈并推送通知
This commit is contained in:
@@ -30,6 +30,6 @@ export function getFeedbackList(params?: { page?: number; pageSize?: number; sta
|
||||
}
|
||||
|
||||
/** 更新反馈状态(管理员) */
|
||||
export function updateFeedbackStatus(id: number, status: string) {
|
||||
return request({ url: `/feedback/${id}/status`, method: 'PUT', data: { status } })
|
||||
export function updateFeedbackStatus(id: number, status: string, admin_reply?: string) {
|
||||
return request({ url: `/feedback/${id}/status`, method: 'PUT', data: { status, admin_reply } })
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="actions">
|
||||
<view v-if="item.status === 'pending'" class="action-btn process" @tap="updateStatus(item, 'processed')">
|
||||
<view v-if="item.status === 'pending'" class="action-btn process" @tap="openReplyModal(item)">
|
||||
<text class="action-text">标记处理</text>
|
||||
</view>
|
||||
<view v-if="item.status === 'pending'" class="action-btn ignore" @tap="updateStatus(item, 'ignored')">
|
||||
@@ -77,6 +77,39 @@
|
||||
<text class="empty-text">暂无反馈</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 回复弹窗 -->
|
||||
<view class="modal-mask" v-if="showReplyModal" @tap="showReplyModal = false">
|
||||
<view class="reply-modal" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">回复反馈</text>
|
||||
<view class="modal-close" @tap="showReplyModal = false">
|
||||
<text class="modal-close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="reply-body">
|
||||
<view class="reply-origin" v-if="replyTarget">
|
||||
<text class="reply-origin-label">用户反馈:</text>
|
||||
<text class="reply-origin-text">{{ replyTarget.content }}</text>
|
||||
</view>
|
||||
<text class="reply-label">回复内容(可选)</text>
|
||||
<textarea
|
||||
class="reply-textarea"
|
||||
v-model="replyText"
|
||||
placeholder="输入回复内容,将通知给用户..."
|
||||
maxlength="500"
|
||||
/>
|
||||
</view>
|
||||
<view class="reply-footer">
|
||||
<view class="reply-btn skip" @tap="handleProcessSkipReply">
|
||||
<text class="reply-btn-text">仅标记处理</text>
|
||||
</view>
|
||||
<view class="reply-btn confirm" @tap="handleProcessWithReply">
|
||||
<text class="reply-btn-text">回复并处理</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -111,6 +144,11 @@ const pageSize = 20
|
||||
const total = ref(0)
|
||||
const initialLoaded = ref(false)
|
||||
|
||||
// 回复弹窗
|
||||
const showReplyModal = ref(false)
|
||||
const replyTarget = ref<Feedback | null>(null)
|
||||
const replyText = ref('')
|
||||
|
||||
const noMore = computed(() => list.value.length >= total.value && total.value > 0)
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -161,6 +199,33 @@ function loadMore() {
|
||||
loadList(false)
|
||||
}
|
||||
|
||||
function openReplyModal(item: Feedback) {
|
||||
replyTarget.value = item
|
||||
replyText.value = ''
|
||||
showReplyModal.value = true
|
||||
}
|
||||
|
||||
async function handleProcessSkipReply() {
|
||||
if (!replyTarget.value) return
|
||||
await doUpdateStatus(replyTarget.value, 'processed')
|
||||
}
|
||||
|
||||
async function handleProcessWithReply() {
|
||||
if (!replyTarget.value) return
|
||||
await doUpdateStatus(replyTarget.value, 'processed', replyText.value.trim() || undefined)
|
||||
}
|
||||
|
||||
async function doUpdateStatus(item: Feedback, status: string, admin_reply?: string) {
|
||||
try {
|
||||
await updateFeedbackStatus(item.id, status, admin_reply)
|
||||
item.status = status
|
||||
showReplyModal.value = false
|
||||
uni.showToast({ title: admin_reply ? '已回复' : '已更新', icon: 'success' })
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStatus(item: Feedback, status: string) {
|
||||
try {
|
||||
await updateFeedbackStatus(item.id, status)
|
||||
@@ -421,4 +486,123 @@ function goBack() { uni.navigateBack() }
|
||||
font-size: $font-md;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
/* 回复弹窗 */
|
||||
.reply-modal {
|
||||
width: 100%;
|
||||
background: $surface;
|
||||
border-radius: $radius-2xl $radius-2xl 0 0;
|
||||
padding-bottom: calc($space-xl + env(safe-area-inset-bottom));
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $space-lg $space-xl $space-md;
|
||||
border-bottom: 2rpx solid $border;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: $font-xl;
|
||||
font-weight: 600;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: $border;
|
||||
&:active { background: darken($border, 5%); }
|
||||
}
|
||||
|
||||
.modal-close-text {
|
||||
font-size: $font-2xl;
|
||||
color: $text-sec;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.reply-body {
|
||||
padding: $space-lg $space-xl;
|
||||
}
|
||||
|
||||
.reply-origin {
|
||||
padding: $space-md;
|
||||
background: $bg;
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $space-lg;
|
||||
}
|
||||
|
||||
.reply-origin-label {
|
||||
font-size: $font-sm;
|
||||
color: $text-muted;
|
||||
display: block;
|
||||
margin-bottom: $space-xs;
|
||||
}
|
||||
|
||||
.reply-origin-text {
|
||||
font-size: $font-md;
|
||||
color: $text-sec;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.reply-label {
|
||||
font-size: $font-base;
|
||||
font-weight: 600;
|
||||
color: $text;
|
||||
display: block;
|
||||
margin-bottom: $space-sm;
|
||||
}
|
||||
|
||||
.reply-textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
padding: $space-md;
|
||||
background: $bg;
|
||||
border-radius: $radius-lg;
|
||||
border: 2rpx solid $border;
|
||||
font-size: $font-md;
|
||||
color: $text;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.reply-footer {
|
||||
display: flex;
|
||||
gap: $space-md;
|
||||
padding: $space-md $space-xl;
|
||||
border-top: 2rpx solid $border;
|
||||
}
|
||||
|
||||
.reply-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: $radius-lg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.skip {
|
||||
background: $bg;
|
||||
border: 2rpx solid $border;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
background: linear-gradient(135deg, $primary, #E67355);
|
||||
}
|
||||
|
||||
&:active { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.reply-btn-text {
|
||||
font-size: $font-md;
|
||||
font-weight: 600;
|
||||
|
||||
.skip & { color: $text-sec; }
|
||||
.confirm & { color: $surface; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user