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

562 lines
15 KiB
Plaintext

<template>
<AdminLayout currentPage="service-autoReply">
<view class="service-container">
<view class="content-card">
<view class="header-row">
<view class="header-title">自动回复管理</view>
<view class="header-actions">
<button class="btn-primary" @click="handleCreate">新增自动回复</button>
</view>
</view>
<view class="filter-section">
<view class="filter-row">
<view class="filter-item">
<text class="filter-label">关键词</text>
<input v-model="searchKeyword" class="filter-input" placeholder="请输入关键词" />
</view>
<view class="filter-item">
<text class="filter-label">状态</text>
<picker v-model="filterStatus" class="picker-wrapper" :range="statusOptions" range-key="label">
<view>{{ filterStatus > -1 ? statusOptions[filterStatus].label : '全部' }}</view>
</picker>
</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-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-keyword">关键词</view>
<view class="cell cell-reply">回复内容</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-keyword">{{ item.keyword }}</view>
<view class="cell cell-reply">{{ item.reply }}</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.updated_at }}</view>
<view class="cell cell-actions">
<button class="btn-action" @click="handleEdit(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 v-if="showModal" class="modal-overlay" @click.self="closeModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ editingId ? '编辑自动回复' : '新增自动回复' }}</text>
<button class="modal-close" @click="closeModal">关闭</button>
</view>
<view class="modal-body">
<view class="form-group">
<label class="form-label">关键词<text class="required">*</text></label>
<input v-model="form.keyword" class="form-input" placeholder="请输入关键词" />
</view>
<view class="form-group">
<label class="form-label">回复内容 <text class="required">*</text></label>
<textarea v-model="form.reply" class="form-textarea" placeholder="请输入回复内容"></textarea>
</view>
<view class="form-group">
<label class="form-label">状态</label>
<picker v-model="form.status" class="picker-wrapper" :range="statusOptions" range-key="label">
<view>{{ statusOptions[form.status].label }}</view>
</picker>
</view>
</view>
<view class="modal-footer">
<button class="btn-cancel" @click="closeModal">取消</button>
<button class="btn-confirm" @click="handleSave">保存</button>
</view>
</view>
</view>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
import { getAutoReplyList, saveAutoReply, deleteAutoReply } 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 showModal = ref<boolean>(false)
const editingId = ref<number | null>(null)
const searchKeyword = ref<string>('')
const filterStatus = ref<number>(-1)
const form = ref<any>({
keyword: '',
reply: '',
status: 1
})
const statusOptions = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize)))
const loadList = async () => {
const res = await getAutoReplyList({
page: page.value,
limit: pageSize,
keyword: searchKeyword.value,
status: filterStatus.value > -1 ? filterStatus.value : undefined
})
list.value = res.items
total.value = res.total
selectedIds.value = []
selectAll.value = false
}
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 && list.value.length > 0
}
const handleCancelSelect = () => {
selectedIds.value = []
selectAll.value = false
}
const handleCreate = () => {
editingId.value = null
form.value = { keyword: '', reply: '', status: 1 }
showModal.value = true
}
const handleEdit = (id: number) => {
const item = list.value.find(i => i.id === id)
if (item) {
editingId.value = id
form.value = { keyword: item.keyword, reply: item.reply, status: item.status }
showModal.value = true
}
}
const handleSave = async () => {
if (!form.value.keyword.trim()) {
uni.showToast({ title: '请输入关键词', icon: 'none' })
return
}
if (!form.value.reply.trim()) {
uni.showToast({ title: '请输入回复内容', icon: 'none' })
return
}
await saveAutoReply({
id: editingId.value,
...form.value
})
uni.showToast({ title: '保存成功', icon: 'success' })
closeModal()
loadList()
}
const closeModal = () => {
showModal.value = false
editingId.value = null
form.value = { keyword: '', reply: '', status: 1 }
}
const handleBatchDelete = () => {
uni.showModal({
title: '批量删除',
content: `确认删除选中的 ${selectedIds.value.length} 条自动回复吗?`,
success: async (res) => {
if (res.confirm) {
for (const id of selectedIds.value) {
await deleteAutoReply(id)
}
uni.showToast({ title: '删除成功', icon: 'success' })
loadList()
}
}
})
}
const handleDelete = async (id: number) => {
uni.showModal({
title: '删除自动回复',
content: '确认删除该自动回复吗?',
success: async (res) => {
if (res.confirm) {
await deleteAutoReply(id)
uni.showToast({ title: '删除成功', icon: 'success' })
loadList()
}
}
})
}
const handleSearch = () => {
page.value = 1
loadList()
}
const handleReset = () => {
searchKeyword.value = ''
filterStatus.value = -1
page.value = 1
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; }
.btn-primary { background: $primary-color; color: #fff; padding: $space-sm $space-lg; border-radius: $radius-sm; border: none; 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;
flex-wrap: wrap;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
gap: $space-sm;
}
.filter-label {
color: $text-secondary;
font-size: $font-size-sm;
min-width: 60px;
}
.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: 100px;
}
.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-delete {
background: $error-color;
}
.btn-cancel-select {
background: #d1d5db;
color: #4b5563;
}
.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; }
.table-row.selected { background: #f3f4f6; }
.cell { padding: $space-sm $space-md; font-size: $font-size-sm; color: $text-primary; display: flex; align-items: center; }
.cell-checkbox { width: 50px; justify-content: center; }
.cell-id { width: 60px; justify-content: center; }
.cell-keyword { width: 140px; }
.cell-reply {
flex: 1;
flex-direction:row
}
.cell-status { width: 100px; justify-content: center; }
.cell-time { width: 180px; }
.cell-actions {
display: flex;
flex-direction:row;
width: 180px;
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; }
/* 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;
}
.form-input {
padding: $space-sm;
border: 1px solid $border-color;
border-radius: $radius-sm;
font-size: $font-size-sm;
color: $text-primary;
background: $background-secondary;
}
.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>