Files
medical-mall/pages/mall/admin/kefu/auto_reply.uvue

575 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="admin-main">
<!-- 头部搜索 -->
<view class="search-card">
<view class="search-row">
<view class="search-item">
<text class="search-label">关键字:</text>
<input class="search-input" v-model="searchQuery" placeholder="请输入关键字" @confirm="handleQuery" />
</view>
<button class="btn-query" @click="handleQuery">查询</button>
<button class="btn-reset" @click="handleReset">重置</button>
</view>
</view>
<!-- 数据表格区域 -->
<view class="table-card border-shadow">
<view class="table-toolbar">
<button class="btn-primary-add" @click="openModal()">添加自动回复</button>
</view>
<view class="table-header-pane">
<view class="th flex-1">序号</view>
<view class="th flex-2">关键字</view>
<view class="th flex-2">回复类型</view>
<view class="th flex-4">回复内容</view>
<view class="th flex-2 text-center">是否开启</view>
<view class="th flex-2 text-center">操作</view>
</view>
<view class="table-body">
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
<text>加载中...</text>
</view>
<view v-else-if="list.length === 0" class="empty-box">
<text class="empty-text">暂无自动回复配置</text>
</view>
<view v-for="(item, index) in list" :key="item.id" class="table-row-item">
<text class="td flex-1 color-9">{{ (page - 1) * pageSize + index + 1 }}</text>
<text class="td flex-2">{{ item.keyword }}</text>
<text class="td flex-2">{{ item.reply_type === 'text' ? '文字消息' : '图片消息' }}</text>
<text class="td flex-4 color-6 truncate">{{ item.content }}</text>
<view class="td flex-2 row-center">
<view class="status-switch-mini" :class="item.status == 1 ? 'active' : ''" @click="toggleStatus(item)">
<view class="switch-dot-mini"></view>
</view>
</view>
<view class="td flex-2 row-center">
<text class="btn-action-blue" @click="openModal(item)">编辑</text>
<view class="v-divider-line"></view>
<text class="btn-action-red" @click="handleDelete(item)">删除</text>
</view>
</view>
</view>
<!-- 分页栏 -->
<view class="pagination-bar">
<text class="page-total">共 {{ total }} 条</text>
<view class="page-nav">
<view class="nav-prev" :class="{ disabled: page <= 1 }" @click="prevPage"><text class="nav-icon"> < </text></view>
<view class="nav-item active"><text class="nav-num">{{ page }}</text></view>
<view class="nav-next" :class="{ disabled: page >= totalPages }" @click="nextPage"><text class="nav-icon"> > </text></view>
</view>
</view>
</view>
<!-- 添加/编辑弹窗 (Centered Modal) -->
<view class="modal-overlay" v-if="showModal" @click="closeModal">
<view class="modal-main-pane" @click.stop>
<view class="modal-header-box">
<text class="modal-title-txt">{{ isEdit ? '编辑自动回复' : '添加自动回复' }}</text>
<text class="modal-close-icon" @click="closeModal">×</text>
</view>
<view class="modal-body-form">
<view class="form-item-box">
<view class="label-box"><text class="form-label font-star">关键字:</text></view>
<view class="val-box">
<input class="input-ctrl" v-model="form.keyword" placeholder="请输入关键字" />
</view>
</view>
<view class="form-item-box">
<view class="label-box"><text class="form-label">回复类型:</text></view>
<view class="val-box row-center-start">
<view class="radio-item" @click="form.reply_type = 'text'">
<view class="radio-circle" :class="form.reply_type === 'text' ? 'radio-checked' : ''">
<view v-if="form.reply_type === 'text'" class="radio-dot-inner"></view>
</view>
<text class="radio-txt">文字消息</text>
</view>
<view class="radio-item" @click="form.reply_type = 'image'">
<view class="radio-circle" :class="form.reply_type === 'image' ? 'radio-checked' : ''">
<view v-if="form.reply_type === 'image'" class="radio-dot-inner"></view>
</view>
<text class="radio-txt">图片消息</text>
</view>
</view>
</view>
<view class="form-item-box">
<view class="label-box"><text class="form-label font-star">回复内容:</text></view>
<view class="val-box">
<textarea class="textarea-ctrl" v-model="form.content" placeholder="请输入回复内容" />
</view>
</view>
<view class="form-item-box">
<view class="label-box"><text class="form-label">状态:</text></view>
<view class="val-box row-center-start">
<view class="radio-item" @click="form.status = 1">
<view class="radio-circle" :class="form.status === 1 ? 'radio-checked' : ''">
<view v-if="form.status === 1" class="radio-dot-inner"></view>
</view>
<text class="radio-txt">开启</text>
</view>
<view class="radio-item" @click="form.status = 0">
<view class="radio-circle" :class="form.status === 0 ? 'radio-checked' : ''">
<view v-if="form.status === 0" class="radio-dot-inner"></view>
</view>
<text class="radio-txt">关闭</text>
</view>
</view>
</view>
</view>
<view class="modal-footer-box">
<button class="btn-foot-cancel" @click="closeModal">取消</button>
<button class="btn-foot-submit" @click="saveReply">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import {
fetchAutoReplyPage,
saveAutoReply as saveAutoReplyService,
deleteAutoReply,
setAutoReplyStatus,
type AutoReply
} from '@/services/admin/kefuService.uts'
const list = ref<AutoReply[]>([])
const loading = ref(false)
const total = ref(0)
const page = ref(1)
const pageSize = ref(15)
const searchQuery = ref('')
const showModal = ref(false)
const isEdit = ref(false)
const currentId = ref<string | null>(null)
const form = reactive({
keyword: '',
reply_type: 'text',
content: '',
status: 1
})
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
return Math.ceil(total.value / pageSize.value)
})
onMounted(() => {
loadData()
})
async function loadData() {
loading.value = true
try {
const res = await fetchAutoReplyPage(page.value, pageSize.value, searchQuery.value || null)
list.value = res.items
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function handleQuery() {
page.value = 1
loadData()
}
function handleReset() {
searchQuery.value = ''
page.value = 1
loadData()
}
function prevPage() {
if (page.value > 1) {
page.value--
loadData()
}
}
function nextPage() {
if (page.value < totalPages.value) {
page.value++
loadData()
}
}
async function toggleStatus(item: AutoReply) {
const targetStatus = item.status === 1 ? 0 : 1
const ok = await setAutoReplyStatus(item.id, targetStatus)
if (ok) {
item.status = targetStatus
uni.showToast({ title: '状态更新成功' })
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
async function handleDelete(item: AutoReply) {
uni.showModal({
title: '提示',
content: `确定删除关键字为 "${item.keyword}" 的自动回复吗?`,
success: async (res) => {
if (res.confirm) {
const ok = await deleteAutoReply(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
}
}
}
})
}
function openModal(item: AutoReply | null = null) {
if (item != null) {
isEdit.value = true
currentId.value = item.id
form.keyword = item.keyword
form.reply_type = item.reply_type
form.content = item.content
form.status = item.status
} else {
isEdit.value = false
currentId.value = null
form.keyword = ''
form.reply_type = 'text'
form.content = ''
form.status = 1
}
showModal.value = true
}
function closeModal() {
showModal.value = false
}
async function saveReply() {
if (!form.keyword) {
uni.showToast({ title: '请输入关键字', icon: 'none' })
return
}
if (!form.content) {
uni.showToast({ title: '请输入回复内容', icon: 'none' })
return
}
const resId = await saveAutoReplyService(
currentId.value,
form.keyword,
form.content,
form.reply_type,
form.status
)
if (resId != null) {
uni.showToast({ title: '保存成功' })
closeModal()
loadData()
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
</script>
<style scoped lang="scss">
.admin-main {
padding: 24px;
background-color: #f0f2f5;
min-height: 100vh;
}
/* 搜索栏样式 */
.search-card {
background-color: #fff;
padding: 20px;
border-radius: 4px;
margin-bottom: 20px;
}
.search-row {
display: flex;
flex-direction: row;
align-items: center;
}
.search-item {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 30px;
}
.search-label {
font-size: 14px;
color: #333;
margin-right: 10px;
white-space: nowrap;
}
.search-input {
width: 240px;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
}
.btn-query {
background-color: #1890ff;
color: #fff;
height: 36px;
line-height: 36px;
padding: 0 20px;
border-radius: 4px;
font-size: 14px;
border: none;
margin-left: 10px;
}
.btn-reset {
background-color: #fff;
color: #666;
border: 1px solid #dcdfe6;
height: 36px;
line-height: 36px;
padding: 0 20px;
border-radius: 4px;
font-size: 14px;
margin-left: 10px;
}
/* 表格区域样式 */
.table-card {
background-color: #fff;
padding: 20px;
border-radius: 4px;
}
.table-toolbar {
margin-bottom: 20px;
}
.btn-primary-add {
background-color: #1890ff;
color: #fff;
height: 34px;
line-height: 34px;
padding: 0 15px;
border-radius: 4px;
font-size: 14px;
border: none;
margin: 0;
}
.table-header-pane {
display: flex;
flex-direction: row;
background-color: #edf1f5;
height: 44px;
align-items: center;
}
.th {
font-size: 13px;
font-weight: bold;
color: #606266;
padding: 0 10px;
}
.table-body {
border-bottom: 1px solid #f0f0f0;
}
.table-row-item {
display: flex;
flex-direction: row;
height: 64px;
align-items: center;
border-left: 1px solid #f0f0f0;
border-right: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.td {
padding: 0 10px;
font-size: 13px;
color: #606266;
}
.empty-box {
padding: 50px 0;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #f0f0f0;
border-top: none;
}
.empty-text { font-size: 14px; color: #999; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.color-9 { color: #999; }
.color-6 { color: #666; }
/* 操作按钮 */
.btn-action-blue { color: #1890ff; font-size: 13px; cursor: pointer; }
.btn-action-red { color: #ff4d4f; font-size: 13px; cursor: pointer; }
.v-divider-line { width: 1px; height: 12px; background-color: #eee; margin: 0 10px; }
/* 状态开关 */
.status-switch-mini {
width: 44px;
height: 22px;
background-color: #dcdfe6;
border-radius: 11px;
position: relative;
transition: background-color 0.3s;
cursor: pointer;
}
.status-switch-mini.active {
background-color: #1890ff;
}
.switch-dot-mini {
width: 18px;
height: 18px;
background-color: #fff;
border-radius: 50%;
position: absolute;
top: 2px;
left: 2px;
transition: left 0.3s;
}
.status-switch-mini.active .switch-dot-mini {
left: 24px;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
}
.modal-main-pane {
width: 520px;
background-color: #fff;
border-radius: 4px;
overflow: hidden;
}
.modal-header-box {
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.modal-title-txt { font-size: 16px; font-weight: 500; color: #333; }
.modal-close-icon { font-size: 22px; color: #999; cursor: pointer; }
.modal-body-form {
padding: 24px;
}
.form-item-box {
display: flex;
flex-direction: row;
margin-bottom: 20px;
align-items: center;
}
.label-box {
width: 90px;
margin-right: 15px;
text-align: right;
}
.form-label { font-size: 14px; color: #606266; }
.font-star::before { content: '*'; color: #ff4d4f; margin-right: 4px; }
.val-box { flex: 1; }
.input-ctrl {
width: 100%;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
}
.textarea-ctrl {
width: 100%;
height: 100px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
}
.radio-item {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 24px;
cursor: pointer;
}
.radio-circle {
width: 16px; height: 16px;
border: 1px solid #dcdfe6;
border-radius: 50%;
margin-right: 8px;
display: flex;
justify-content: center;
align-items: center;
}
.radio-checked { border-color: #1890ff; }
.radio-dot-inner {
width: 8px; height: 8px;
background-color: #1890ff;
border-radius: 50%;
}
.radio-txt { font-size: 14px; color: #606266; }
.modal-footer-box {
padding: 15px 20px;
border-top: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.btn-foot-cancel {
background-color: #fff; border: 1px solid #dcdfe6; color: #606266;
padding: 0 20px; height: 32px; line-height: 32px; border-radius: 4px; font-size: 14px;
margin-right: 12px;
}
.btn-foot-submit {
background-color: #1890ff; color: #fff; border: none;
padding: 0 20px; height: 32px; line-height: 32px; border-radius: 4px; font-size: 14px;
}
/* 布局辅助 */
.flex-1 { flex: 1; }
.flex-2 { flex: 2; }
.flex-4 { flex: 4; }
.row-center { display: flex; flex-direction: row; align-items: center; justify-content: center; }
.row-center-start { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; }
.text-center { text-align: center; }
/* 分页 */
.pagination-bar {
padding: 15px 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.page-total { font-size: 13px; color: #606266; margin-right: 15px; }
.page-nav { display: flex; flex-direction: row; align-items: center; }
.nav-prev, .nav-next, .nav-item {
width: 30px; height: 30px; border: 1px solid #dcdee2;
display: flex; align-items: center; justify-content: center;
border-radius: 4px; margin: 0 4px; cursor: pointer;
}
.nav-item.active { background-color: #1890ff; border-color: #1890ff; }
.nav-item.active .nav-num { color: #fff; }
.nav-num, .nav-icon { font-size: 13px; color: #606266; }
.disabled { cursor: not-allowed; opacity: 0.5; }
</style>