562 lines
15 KiB
Plaintext
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="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>
|
|
|
|
|