545 lines
14 KiB
Plaintext
545 lines
14 KiB
Plaintext
<template>
|
|
<AdminLayout currentPage="service-message">
|
|
<view class="service-container">
|
|
<view class="content-card">
|
|
<view class="filter-section">
|
|
<view class="filter-row">
|
|
<view class="filter-item">
|
|
<text class="filter-label">状态</text>
|
|
<picker
|
|
:range="statusOptions"
|
|
range-key="label"
|
|
:value="filterStatus"
|
|
@change="(e) => filterStatus = statusOptions[e.detail.value].value"
|
|
>
|
|
<view class="picker-wrapper">
|
|
<text>{{ statusOptions.find(s => s.value === filterStatus)?.label || '全部' }}</text>
|
|
</view>
|
|
</picker>
|
|
</view>
|
|
<view class="filter-item">
|
|
<text class="filter-label">用户名</text>
|
|
<input v-model="keyword" class="filter-input" placeholder="请输入用户名" />
|
|
</view>
|
|
<button class="btn-search" @click="handleSearch">搜索</button>
|
|
<button class="btn-reset" @click="handleReset">重置</button>
|
|
</view>
|
|
<view v-if="selectedIds.length > 0" class="batch-actions">
|
|
<text class="batch-text">已选择 {{ selectedIds.length }} 条</text>
|
|
<button class="btn-batch-reply" @click="handleBatchReply">批量标记已读</button>
|
|
<button class="btn-batch-delete" @click="handleBatchDelete">批量删除</button>
|
|
<button class="btn-cancel-select" @click="handleCancelSelect">取消选择</button>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="table">
|
|
<view class="table-header">
|
|
<view class="cell cell-checkbox">
|
|
<input type="checkbox" :checked="selectAll" @change="handleSelectAll" />
|
|
</view>
|
|
<view class="cell cell-id">ID</view>
|
|
<view class="cell cell-user">用户</view>
|
|
<view class="cell cell-contact">联系方式</view>
|
|
<view class="cell cell-content">消息内容</view>
|
|
<view class="cell cell-status">状态</view>
|
|
<view class="cell cell-time">时间</view>
|
|
<view class="cell cell-actions">操作</view>
|
|
</view>
|
|
<view v-if="list.length === 0" class="empty">暂无数据</view>
|
|
<view v-for="item in list" :key="item.id" class="table-row" :class="{ selected: selectedIds.includes(item.id) }">
|
|
<view class="cell cell-checkbox">
|
|
<input type="checkbox" :checked="selectedIds.includes(item.id)" @change="handleSelectItem(item.id)" />
|
|
</view>
|
|
<view class="cell cell-id">{{ item.id }}</view>
|
|
<view class="cell cell-user">{{ item.user }}</view>
|
|
<view class="cell cell-contact">{{ item.contact }}</view>
|
|
<view class="cell cell-content">{{ item.content }}</view>
|
|
<view class="cell cell-status">
|
|
<text class="badge" :class="{ on: item.status === 1 }">{{ item.status === 1 ? '已读' : '未读' }}</text>
|
|
</view>
|
|
<view class="cell cell-time">{{ item.created_at }}</view>
|
|
<view class="cell cell-actions">
|
|
<button class="btn-action" @click="handleReply(item.id)">回复</button>
|
|
<button class="btn-action btn-danger" @click="handleDelete(item.id)">删除</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="pagination">
|
|
<button class="btn-page" :disabled="page === 1" @click="prevPage">上一页</button>
|
|
<text class="page-text">第 {{ page }} 页</text>
|
|
<button class="btn-page" :disabled="page >= totalPage" @click="nextPage">下一页</button>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 回复消息 -->
|
|
<view v-if="showReplyModal" class="modal-overlay" @click.stop="closeReplyModal">
|
|
<view class="modal-content" @click.stop>
|
|
<view class="modal-header">
|
|
<text class="modal-title">回复消息</text>
|
|
<button class="modal-close" @click="closeReplyModal">关闭</button>
|
|
</view>
|
|
<view class="modal-body">
|
|
<view class="form-group">
|
|
<text class="form-label">消息内容</text>
|
|
<view class="reply-content-box">{{ replyingMessage.content }}</view>
|
|
</view>
|
|
<view class="form-group">
|
|
<text class="form-label">回复内容 <text class="required">*</text></text>
|
|
<textarea v-model="replyForm.reply" class="form-textarea" placeholder="请输入回复内容"></textarea>
|
|
</view>
|
|
</view>
|
|
<view class="modal-footer">
|
|
<button class="btn-cancel" @click="closeReplyModal">取消</button>
|
|
<button class="btn-confirm" @click="handleSendReply">发送回复</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</AdminLayout>
|
|
</template>
|
|
<script setup lang="uts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
|
import { getMessageList, replyMessage, deleteMessage } from './service.uts'
|
|
|
|
const list = ref<any[]>([])
|
|
const page = ref<number>(1)
|
|
const pageSize = 10
|
|
const total = ref<number>(0)
|
|
const selectedIds = ref<number[]>([])
|
|
const selectAll = ref<boolean>(false)
|
|
const showReplyModal = ref<boolean>(false)
|
|
const keyword = ref<string>('')
|
|
const filterStatus = ref<any>('')
|
|
|
|
const statusOptions = [
|
|
{ label: '全部', value: '' },
|
|
{ label: '已读', value: 1 },
|
|
{ label: '未读', value: 0 }
|
|
]
|
|
|
|
const replyingMessage = ref<any>({ id: null, content: '' })
|
|
const replyForm = ref({ reply: '' })
|
|
|
|
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize)))
|
|
|
|
const loadList = async () => {
|
|
const res = await getMessageList({ page: page.value, limit: pageSize, keyword: keyword.value, status: filterStatus.value })
|
|
list.value = res.items
|
|
total.value = res.total
|
|
selectAll.value = false
|
|
selectedIds.value = []
|
|
}
|
|
|
|
const handleSearch = () => {
|
|
page.value = 1
|
|
loadList()
|
|
}
|
|
|
|
const handleReset = () => {
|
|
keyword.value = ''
|
|
filterStatus.value = ''
|
|
page.value = 1
|
|
selectedIds.value = []
|
|
loadList()
|
|
}
|
|
|
|
const handleSelectAll = () => {
|
|
selectAll.value = !selectAll.value
|
|
if (selectAll.value) {
|
|
selectedIds.value = list.value.map(item => item.id)
|
|
} else {
|
|
selectedIds.value = []
|
|
}
|
|
}
|
|
|
|
const handleSelectItem = (id: number) => {
|
|
const index = selectedIds.value.indexOf(id)
|
|
if (index > -1) {
|
|
selectedIds.value.splice(index, 1)
|
|
} else {
|
|
selectedIds.value.push(id)
|
|
}
|
|
selectAll.value = selectedIds.value.length === list.value.length
|
|
}
|
|
|
|
const handleCancelSelect = () => {
|
|
selectedIds.value = []
|
|
selectAll.value = false
|
|
}
|
|
|
|
const handleReply = (id: number) => {
|
|
const item = list.value.find(i => i.id === id)
|
|
if (item) {
|
|
replyingMessage.value = item
|
|
replyForm.value.reply = ''
|
|
showReplyModal.value = true
|
|
}
|
|
}
|
|
|
|
const handleSendReply = async () => {
|
|
if (!replyForm.value.reply.trim()) {
|
|
uni.showToast({ title: '请输入回复内容', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
await replyMessage(replyingMessage.value.id, { reply: replyForm.value.reply })
|
|
uni.showToast({ title: '回复成功', icon: 'success' })
|
|
closeReplyModal()
|
|
loadList()
|
|
}
|
|
|
|
const closeReplyModal = () => {
|
|
showReplyModal.value = false
|
|
replyingMessage.value = { id: null, content: '' }
|
|
replyForm.value.reply = ''
|
|
}
|
|
|
|
const handleBatchReply = () => {
|
|
uni.showToast({ title: '批量标记已读功能开发中', icon: 'none' })
|
|
}
|
|
|
|
const handleBatchDelete = () => {
|
|
uni.showModal({
|
|
title: '批量删除',
|
|
content: `确认删除选中的 ${selectedIds.value.length} 条消息吗?`,
|
|
success: async (res) => {
|
|
if (res.confirm) {
|
|
for (const id of selectedIds.value) {
|
|
await deleteMessage(id)
|
|
}
|
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
loadList()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleDelete = async (id: number) => {
|
|
uni.showModal({
|
|
title: '删除消息',
|
|
content: '确认删除该消息吗?',
|
|
success: async (res) => {
|
|
if (res.confirm) {
|
|
await deleteMessage(id)
|
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
loadList()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const prevPage = () => {
|
|
if (page.value > 1) {
|
|
page.value--
|
|
loadList()
|
|
}
|
|
}
|
|
|
|
const nextPage = () => {
|
|
if (page.value < totalPage.value) {
|
|
page.value++
|
|
loadList()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadList()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@import '@/uni.scss';
|
|
|
|
.service-container {
|
|
display: flex;
|
|
flex-direction: row;
|
|
width: 100%;
|
|
padding: $space-lg;
|
|
background: $background-secondary;
|
|
}
|
|
|
|
.content-card {
|
|
flex: 1;
|
|
background: #fff;
|
|
border-radius: $radius-lg;
|
|
box-shadow: $shadow-sm;
|
|
padding: $space-lg;
|
|
}
|
|
|
|
.header-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
width: 100%;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: $space-lg;
|
|
}
|
|
.header-title { font-size: $font-size-lg; font-weight: 600; color: $text-primary; }
|
|
|
|
.table { border: 1px solid $border-color; border-radius: $radius-sm; overflow: hidden; }
|
|
.table-header,
|
|
.table-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
.table-header { background: $background-tertiary; font-weight: 600; }
|
|
.table-row { border-top: 1px solid $border-color; }
|
|
|
|
.cell { padding: $space-sm $space-md; font-size: $font-size-sm; color: $text-primary; display: flex; align-items: center; }
|
|
.cell-id { width: 60px; justify-content: center; }
|
|
.cell-user { width: 100px; }
|
|
.cell-contact { width: 140px; }
|
|
.cell-content { flex: 1; }
|
|
.cell-status { width: 100px; justify-content: center; }
|
|
.cell-time { width: 180px; }
|
|
.cell-actions { width: 160px; gap: $space-sm; justify-content: center; }
|
|
|
|
.badge { padding: 2px 10px; border-radius: $radius-sm; background: lighten($error-color, 40%); color: $error-color; font-size: $font-size-xs; }
|
|
.badge.on { background: lighten($success-color, 40%); color: $success-color; }
|
|
|
|
.btn-action { border: 1px solid $primary-color; color: $primary-color; background: transparent; padding: 4px 10px; border-radius: $radius-sm; font-size: $font-size-xs; }
|
|
.btn-action.btn-danger { border-color: $error-color; color: $error-color; }
|
|
|
|
.empty { padding: $space-xl; text-align: center; color: $text-tertiary; }
|
|
|
|
.pagination {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: $space-md;
|
|
margin-top: $space-lg;
|
|
}
|
|
.btn-page { padding: $space-xs $space-md; border: 1px solid $border-color; border-radius: $radius-sm; background: #fff; }
|
|
.page-text { color: $text-secondary; font-size: $font-size-sm; }
|
|
|
|
.filter-section {
|
|
margin-bottom: $space-lg;
|
|
}
|
|
|
|
.filter-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
width: 100%;
|
|
align-items: center;
|
|
gap: $space-md;
|
|
margin-bottom: $space-lg;
|
|
}
|
|
|
|
.filter-item {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: $space-sm;
|
|
}
|
|
|
|
.filter-label {
|
|
color: $text-secondary;
|
|
font-size: $font-size-sm;
|
|
}
|
|
|
|
.picker-wrapper {
|
|
padding: $space-xs $space-sm;
|
|
border: 1px solid $border-color;
|
|
border-radius: $radius-sm;
|
|
}
|
|
|
|
.filter-input {
|
|
padding: $space-xs $space-sm;
|
|
border: 1px solid $border-color;
|
|
border-radius: $radius-sm;
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.btn-search {
|
|
background: $primary-color;
|
|
color: #fff;
|
|
padding: $space-xs $space-lg;
|
|
border: none;
|
|
border-radius: $radius-sm;
|
|
font-size: $font-size-sm;
|
|
}
|
|
|
|
.btn-reset {
|
|
background: transparent;
|
|
color: $primary-color;
|
|
border: 1px solid $primary-color;
|
|
padding: $space-xs $space-lg;
|
|
border-radius: $radius-sm;
|
|
font-size: $font-size-sm;
|
|
}
|
|
|
|
.batch-actions {
|
|
display: flex;
|
|
flex-direction: row;
|
|
width: 100%;
|
|
align-items: center;
|
|
gap: $space-md;
|
|
padding: $space-md;
|
|
background: #fef3c7;
|
|
border-radius: $radius-sm;
|
|
margin-top: $space-md;
|
|
}
|
|
|
|
.batch-text {
|
|
font-size: $font-size-sm;
|
|
color: #92400e;
|
|
min-width: 120px;
|
|
}
|
|
|
|
.btn-batch-reply,
|
|
.btn-batch-delete,
|
|
.btn-cancel-select {
|
|
padding: $space-xs $space-md;
|
|
border-radius: $radius-sm;
|
|
border: none;
|
|
font-size: $font-size-xs;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-batch-reply {
|
|
background: $primary-color;
|
|
}
|
|
|
|
.btn-batch-delete {
|
|
background: $error-color;
|
|
}
|
|
|
|
.btn-cancel-select {
|
|
background: #d1d5db;
|
|
color: #4b5563;
|
|
}
|
|
|
|
.table-row.selected {
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
.cell-checkbox {
|
|
width: 50px;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Modal Styles */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
background: #fff;
|
|
border-radius: $radius-lg;
|
|
width: 90%;
|
|
max-width: 500px;
|
|
max-height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: $space-lg;
|
|
border-bottom: 1px solid $border-color;
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: $font-size-base;
|
|
font-weight: 600;
|
|
color: $text-primary;
|
|
}
|
|
|
|
.modal-close {
|
|
background: transparent;
|
|
border: none;
|
|
font-size: 28px;
|
|
color: $text-tertiary;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: $space-lg;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: $space-lg;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: $space-sm;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: $font-size-sm;
|
|
font-weight: 500;
|
|
color: $text-primary;
|
|
}
|
|
|
|
.required {
|
|
color: $error-color;
|
|
}
|
|
|
|
.reply-content-box {
|
|
padding: $space-sm;
|
|
background: $background-secondary;
|
|
border-radius: $radius-sm;
|
|
font-size: $font-size-sm;
|
|
color: $text-primary;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.form-textarea {
|
|
padding: $space-sm;
|
|
border: 1px solid $border-color;
|
|
border-radius: $radius-sm;
|
|
font-size: $font-size-sm;
|
|
color: $text-primary;
|
|
background: $background-secondary;
|
|
min-height: 120px;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
gap: $space-md;
|
|
justify-content: flex-end;
|
|
padding: $space-lg;
|
|
border-top: 1px solid $border-color;
|
|
background: $background-secondary;
|
|
}
|
|
|
|
.btn-cancel,
|
|
.btn-confirm {
|
|
padding: $space-sm $space-lg;
|
|
border-radius: $radius-sm;
|
|
border: none;
|
|
font-size: $font-size-sm;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-cancel {
|
|
background: $background-tertiary;
|
|
color: $text-primary;
|
|
}
|
|
|
|
.btn-confirm {
|
|
background: $primary-color;
|
|
color: #fff;
|
|
}
|
|
</style>
|
|
|
|
|