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) {
|
export function updateFeedbackStatus(id: number, status: string, admin_reply?: string) {
|
||||||
return request({ url: `/feedback/${id}/status`, method: 'PUT', data: { status } })
|
return request({ url: `/feedback/${id}/status`, method: 'PUT', data: { status, admin_reply } })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<view class="actions">
|
<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>
|
<text class="action-text">标记处理</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="item.status === 'pending'" class="action-btn ignore" @tap="updateStatus(item, 'ignored')">
|
<view v-if="item.status === 'pending'" class="action-btn ignore" @tap="updateStatus(item, 'ignored')">
|
||||||
@@ -77,6 +77,39 @@
|
|||||||
<text class="empty-text">暂无反馈</text>
|
<text class="empty-text">暂无反馈</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -111,6 +144,11 @@ const pageSize = 20
|
|||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const initialLoaded = ref(false)
|
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)
|
const noMore = computed(() => list.value.length >= total.value && total.value > 0)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -161,6 +199,33 @@ function loadMore() {
|
|||||||
loadList(false)
|
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) {
|
async function updateStatus(item: Feedback, status: string) {
|
||||||
try {
|
try {
|
||||||
await updateFeedbackStatus(item.id, status)
|
await updateFeedbackStatus(item.id, status)
|
||||||
@@ -421,4 +486,123 @@ function goBack() { uni.navigateBack() }
|
|||||||
font-size: $font-md;
|
font-size: $font-md;
|
||||||
color: $text-muted;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user