Files
medical-mall/pages/mall/admin/service/message.uvue
2026-02-02 20:07:37 +08:00

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>