feat(admin): complete full integration of kefu, finance, product and order modules with real RPC data streams
This commit is contained in:
@@ -3,29 +3,23 @@
|
||||
<!-- 头部搜索 -->
|
||||
<view class="search-card">
|
||||
<view class="search-row">
|
||||
<view class="search-item">
|
||||
<text class="search-label">回复类型:</text>
|
||||
<view class="mock-select">
|
||||
<text class="select-val">请选择</text>
|
||||
<text class="arrow-down-icon">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-item">
|
||||
<text class="search-label">关键字:</text>
|
||||
<input class="search-input" placeholder="请输入关键字" />
|
||||
<input class="search-input" v-model="searchQuery" placeholder="请输入关键字" @confirm="handleQuery" />
|
||||
</view>
|
||||
<button class="btn-query">查询</button>
|
||||
<button class="btn-query" @click="handleQuery">查询</button>
|
||||
<button class="btn-reset" @click="handleReset">重置</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据表格区域 -->
|
||||
<view class="table-card">
|
||||
<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">ID</view>
|
||||
<view class="th flex-1">序号</view>
|
||||
<view class="th flex-2">关键字</view>
|
||||
<view class="th flex-2">回复类型</view>
|
||||
<view class="th flex-4">回复内容</view>
|
||||
@@ -34,33 +28,46 @@
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-if="list.length === 0" class="empty-box">
|
||||
<text class="empty-text">暂无数据</text>
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-for="(item, index) in list" :key="index" class="table-row-item">
|
||||
<text class="td flex-1 color-9">{{ item.id }}</text>
|
||||
<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.type === 'text' ? '文字消息' : '图片消息' }}</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 ? 'active' : ''" @click="toggleStatus(index)">
|
||||
<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="deleteItem(index)">删除</text>
|
||||
<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">客服自动回复</text>
|
||||
<text class="modal-title-txt">{{ isEdit ? '编辑自动回复' : '添加自动回复' }}</text>
|
||||
<text class="modal-close-icon" @click="closeModal">×</text>
|
||||
</view>
|
||||
<view class="modal-body-form">
|
||||
@@ -73,15 +80,15 @@
|
||||
<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.type = 'text'">
|
||||
<view class="radio-circle" :class="form.type === 'text' ? 'radio-checked' : ''">
|
||||
<view v-if="form.type === 'text'" class="radio-dot-inner"></view>
|
||||
<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.type = 'image'">
|
||||
<view class="radio-circle" :class="form.type === 'image' ? 'radio-checked' : ''">
|
||||
<view v-if="form.type === 'image'" class="radio-dot-inner"></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>
|
||||
@@ -90,21 +97,21 @@
|
||||
<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.content" placeholder="请输入回复内容" />
|
||||
<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 = true">
|
||||
<view class="radio-circle" :class="form.status ? 'radio-checked' : ''">
|
||||
<view v-if="form.status" class="radio-dot-inner"></view>
|
||||
<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 = false">
|
||||
<view class="radio-circle" :class="!form.status ? 'radio-checked' : ''">
|
||||
<view v-if="!form.status" class="radio-dot-inner"></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>
|
||||
@@ -121,43 +128,122 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import {
|
||||
fetchAutoReplyPage,
|
||||
saveAutoReply as saveAutoReplyService,
|
||||
deleteAutoReply,
|
||||
setAutoReplyStatus,
|
||||
type AutoReply
|
||||
} from '@/services/admin/kefuService.uts'
|
||||
|
||||
interface AutoReplyItem {
|
||||
id: number;
|
||||
keyword: string;
|
||||
type: string; // 'text' | 'image'
|
||||
content: string;
|
||||
status: boolean;
|
||||
}
|
||||
|
||||
const list = reactive<AutoReplyItem[]>([])
|
||||
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 editIndex = ref(-1)
|
||||
const currentId = ref<string | null>(null)
|
||||
|
||||
const form = reactive({
|
||||
keyword: '',
|
||||
type: 'text',
|
||||
reply_type: 'text',
|
||||
content: '',
|
||||
status: true
|
||||
status: 1
|
||||
})
|
||||
|
||||
function openModal(item: AutoReplyItem | null = null) {
|
||||
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.type = item.type
|
||||
form.reply_type = item.reply_type
|
||||
form.content = item.content
|
||||
form.status = item.status
|
||||
editIndex.value = list.indexOf(item)
|
||||
} else {
|
||||
isEdit.value = false
|
||||
currentId.value = null
|
||||
form.keyword = ''
|
||||
form.type = 'text'
|
||||
form.reply_type = 'text'
|
||||
form.content = ''
|
||||
form.status = true
|
||||
form.status = 1
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
@@ -166,7 +252,7 @@ function closeModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
function saveReply() {
|
||||
async function saveReply() {
|
||||
if (!form.keyword) {
|
||||
uni.showToast({ title: '请输入关键字', icon: 'none' })
|
||||
return
|
||||
@@ -176,40 +262,21 @@ function saveReply() {
|
||||
return
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
const item = list[editIndex.value]
|
||||
item.keyword = form.keyword
|
||||
item.type = form.type
|
||||
item.content = form.content
|
||||
item.status = form.status
|
||||
const resId = await saveAutoReplyService(
|
||||
currentId.value,
|
||||
form.keyword,
|
||||
form.content,
|
||||
form.reply_type,
|
||||
form.status
|
||||
)
|
||||
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
closeModal()
|
||||
loadData()
|
||||
} else {
|
||||
list.unshift({
|
||||
id: Date.now() % 10000,
|
||||
keyword: form.keyword,
|
||||
type: form.type,
|
||||
content: form.content,
|
||||
status: form.status
|
||||
})
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
closeModal()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
}
|
||||
|
||||
function toggleStatus(index: number) {
|
||||
list[index].status = !list[index].status
|
||||
}
|
||||
|
||||
function deleteItem(index: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定删除该自动回复吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
list.splice(index, 1)
|
||||
uni.showToast({ title: '删除成功', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -244,23 +311,10 @@ function deleteItem(index: number) {
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mock-select {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.select-val { font-size: 14px; color: #c0c4cc; }
|
||||
.arrow-down-icon { font-size: 10px; color: #c0c4cc; }
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
width: 240px;
|
||||
height: 36px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
@@ -269,14 +323,25 @@ function deleteItem(index: number) {
|
||||
.btn-query {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
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 {
|
||||
@@ -290,8 +355,8 @@ function deleteItem(index: number) {
|
||||
.btn-primary-add {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
padding: 0 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
@@ -307,9 +372,9 @@ function deleteItem(index: number) {
|
||||
align-items: center;
|
||||
}
|
||||
.th {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
color: #606266;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.table-body {
|
||||
@@ -318,7 +383,7 @@ function deleteItem(index: number) {
|
||||
.table-row-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 54px;
|
||||
height: 64px;
|
||||
align-items: center;
|
||||
border-left: 1px solid #f0f0f0;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
@@ -326,7 +391,7 @@ function deleteItem(index: number) {
|
||||
}
|
||||
.td {
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
@@ -345,8 +410,8 @@ function deleteItem(index: number) {
|
||||
.color-6 { color: #666; }
|
||||
|
||||
/* 操作按钮 */
|
||||
.btn-action-blue { color: #1890ff; font-size: 14px; cursor: pointer; }
|
||||
.btn-action-red { color: #ff4d4f; font-size: 14px; cursor: pointer; }
|
||||
.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; }
|
||||
|
||||
/* 状态开关 */
|
||||
@@ -357,6 +422,7 @@ function deleteItem(index: number) {
|
||||
border-radius: 11px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-switch-mini.active {
|
||||
background-color: #1890ff;
|
||||
@@ -386,7 +452,7 @@ function deleteItem(index: number) {
|
||||
align-items: center;
|
||||
}
|
||||
.modal-main-pane {
|
||||
width: 600px;
|
||||
width: 520px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@@ -403,16 +469,16 @@ function deleteItem(index: number) {
|
||||
.modal-close-icon { font-size: 22px; color: #999; cursor: pointer; }
|
||||
|
||||
.modal-body-form {
|
||||
padding: 30px;
|
||||
padding: 24px;
|
||||
}
|
||||
.form-item-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.label-box {
|
||||
width: 100px;
|
||||
width: 90px;
|
||||
margin-right: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -428,12 +494,20 @@ function deleteItem(index: number) {
|
||||
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: 30px;
|
||||
margin-right: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.radio-circle {
|
||||
@@ -463,7 +537,7 @@ function deleteItem(index: number) {
|
||||
.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: 15px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.btn-foot-submit {
|
||||
background-color: #1890ff; color: #fff; border: none;
|
||||
@@ -477,4 +551,24 @@ function deleteItem(index: number) {
|
||||
.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>
|
||||
|
||||
@@ -17,21 +17,21 @@
|
||||
</view>
|
||||
<view class="input-box flex-row">
|
||||
<view class="radio-group">
|
||||
<view class="radio-item" @click="configType = 'system'">
|
||||
<view :class="['radio-circle', configType === 'system' ? 'checked' : '']">
|
||||
<view v-if="configType === 'system'" class="radio-inner"></view>
|
||||
<view class="radio-item" @click="form.type = 'system'">
|
||||
<view :class="['radio-circle', form.type === 'system' ? 'checked' : '']">
|
||||
<view v-if="form.type === 'system'" class="radio-inner"></view>
|
||||
</view>
|
||||
<text class="radio-label">系统客服</text>
|
||||
</view>
|
||||
<view class="radio-item" @click="configType = 'phone'">
|
||||
<view :class="['radio-circle', configType === 'phone' ? 'checked' : '']">
|
||||
<view v-if="configType === 'phone'" class="radio-inner"></view>
|
||||
<view class="radio-item" @click="form.type = 'phone'">
|
||||
<view :class="['radio-circle', form.type === 'phone' ? 'checked' : '']">
|
||||
<view v-if="form.type === 'phone'" class="radio-inner"></view>
|
||||
</view>
|
||||
<text class="radio-label">拨打电话</text>
|
||||
</view>
|
||||
<view class="radio-item" @click="configType = 'link'">
|
||||
<view :class="['radio-circle', configType === 'link' ? 'checked' : '']">
|
||||
<view v-if="configType === 'link'" class="radio-inner"></view>
|
||||
<view class="radio-item" @click="form.type = 'link'">
|
||||
<view :class="['radio-circle', form.type === 'link' ? 'checked' : '']">
|
||||
<view v-if="form.type === 'link'" class="radio-inner"></view>
|
||||
</view>
|
||||
<text class="radio-label">跳转链接</text>
|
||||
</view>
|
||||
@@ -47,7 +47,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 动态显示内容 -->
|
||||
<template v-if="configType === 'system'">
|
||||
<template v-if="form.type === 'system'">
|
||||
<view class="form-item align-start">
|
||||
<view class="label-box pt-10">
|
||||
<text class="label-txt">客服反馈:</text>
|
||||
@@ -55,34 +55,34 @@
|
||||
<view class="input-box">
|
||||
<textarea
|
||||
class="textarea-base"
|
||||
v-model="feedbackText"
|
||||
v-model="form.feedback"
|
||||
placeholder="请输入客服反馈内容"
|
||||
/>
|
||||
<text class="input-tip">暂无客服在线是,联系客服跳转的客服反馈页面的显示文字</text>
|
||||
<text class="input-tip">暂无客服在线时,联系客服跳转的客服反馈页面的显示文字</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="configType === 'phone'">
|
||||
<template v-else-if="form.type === 'phone'">
|
||||
<view class="form-item">
|
||||
<view class="label-box">
|
||||
<text class="label-txt">客服电话:</text>
|
||||
</view>
|
||||
<view class="input-box">
|
||||
<input class="input-base" v-model="phoneNumber" placeholder="请输入客服电话" />
|
||||
<text class="input-tip">客服类型选择不打电话是,用户点击联系客服的联系电话</text>
|
||||
<input class="input-base" v-model="form.phone" placeholder="请输入客服电话" />
|
||||
<text class="input-tip">用户点击联系客服时的拨打号码</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="configType === 'link'">
|
||||
<template v-else-if="form.type === 'link'">
|
||||
<view class="form-item">
|
||||
<view class="label-box">
|
||||
<text class="label-txt">跳转链接:</text>
|
||||
</view>
|
||||
<view class="input-box">
|
||||
<input class="input-base" v-model="linkUrl" placeholder="请输入外部链接地址" />
|
||||
<text class="input-tip">客服类型选择跳转链接时,用户点击联系客服跳转的外部链接</text>
|
||||
<input class="input-base" v-model="form.link" placeholder="请输入外部链接地址" />
|
||||
<text class="input-tip">用户点击联系客服时跳转的外部链接</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -99,24 +99,44 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { getKefuSettings, saveKefuSettings, type KefuConfig } from '@/services/admin/kefuService.uts'
|
||||
|
||||
const configType = ref('system') // system, phone, link
|
||||
const feedbackText = ref('尊敬的用户,客服当前不在线,有问题请留言,我们会第一时间进行处理!!!')
|
||||
const phoneNumber = ref('4008888794')
|
||||
const linkUrl = ref('')
|
||||
const form = reactive({
|
||||
type: 'system',
|
||||
feedback: '',
|
||||
phone: '',
|
||||
link: ''
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
console.log('提交配置', {
|
||||
type: configType.value,
|
||||
feedback: feedbackText.value,
|
||||
phone: phoneNumber.value,
|
||||
link: linkUrl.value
|
||||
})
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await getKefuSettings()
|
||||
if (res != null) {
|
||||
form.type = res.type
|
||||
form.feedback = res.feedback
|
||||
form.phone = res.phone
|
||||
form.link = res.link
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
uni.showLoading({ title: '正在保存...' })
|
||||
try {
|
||||
const ok = await saveKefuSettings(form as KefuConfig)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '系统错误', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,21 +3,14 @@
|
||||
<view class="content-body">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-item">
|
||||
<text class="label-txt">留言时间:</text>
|
||||
<view class="date-picker-mock">
|
||||
<text class="placeholder">开始日期</text>
|
||||
<text class="sep">-</text>
|
||||
<text class="placeholder">结束日期</text>
|
||||
<text class="calendar-icon">📅</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-item">
|
||||
<text class="label-txt">留言信息:</text>
|
||||
<input class="search-input" placeholder="请输入用户昵称/电话/留言内容搜索" v-model="searchQuery" />
|
||||
<input class="search-input" placeholder="请输入用户昵称/电话/留言内容搜索" v-model="searchQuery" @confirm="handleQuery" />
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label-txt">状态:</text>
|
||||
<uni-data-select v-model="statusValue" :localdata="statusOptions" style="width: 120px;" @change="handleQuery" />
|
||||
</view>
|
||||
|
||||
<view class="btn-query" @click="handleQuery">
|
||||
<text class="query-txt">查询</text>
|
||||
</view>
|
||||
@@ -26,40 +19,54 @@
|
||||
<!-- 数据表格 -->
|
||||
<view class="table-card border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th col-id"><text class="th-txt">ID</text></view>
|
||||
<view class="th col-nick"><text class="th-txt">昵称</text></view>
|
||||
<view class="th col-phone"><text class="th-txt">电话</text></view>
|
||||
<view class="th col-content"><text class="th-txt">内容</text></view>
|
||||
<view class="th col-id"><text class="th-txt">序号</text></view>
|
||||
<view class="th col-nick"><text class="th-txt">用户信息</text></view>
|
||||
<view class="th col-phone"><text class="th-txt">联系电话</text></view>
|
||||
<view class="th col-content"><text class="th-txt">留言内容</text></view>
|
||||
<view class="th col-status"><text class="th-txt">状态</text></view>
|
||||
<view class="th col-time"><text class="th-txt">时间</text></view>
|
||||
<view class="th col-time"><text class="th-txt">留言时间</text></view>
|
||||
<view class="th col-op"><text class="th-txt">操作</text></view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="item in feedbackList" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view class="td col-nick"><text class="td-txt">{{ item.nickname }}</text></view>
|
||||
<view class="td col-phone"><text class="td-txt">{{ item.phone }}</text></view>
|
||||
<view class="td col-content"><text class="td-txt ellipsis">{{ item.content }}</text></view>
|
||||
<view class="td col-status">
|
||||
<text class="status-tag">{{ item.status }}</text>
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="feedbackList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无留言记录</text>
|
||||
</view>
|
||||
<view v-else class="table-row" v-for="(item, index) in feedbackList" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td col-nick">
|
||||
<view class="u-info">
|
||||
<text class="td-txt">{{ item.nickname || '匿名' }}</text>
|
||||
<text class="u-account" v-if="item.user_account">账号:{{ item.user_account }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></view>
|
||||
<view class="td col-phone"><text class="td-txt">{{ item.phone || '-' }}</text></view>
|
||||
<view class="td col-content">
|
||||
<text class="td-txt ellipsis">{{ item.content }}</text>
|
||||
</view>
|
||||
<view class="td col-status">
|
||||
<text class="status-tag" :class="item.status == 1 ? 'processed' : 'pending'">
|
||||
{{ item.status == 1 ? '已处理' : '未处理' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.created_at.substring(0, 16).replace('T', ' ') }}</text></view>
|
||||
<view class="td col-op">
|
||||
<text class="btn-link" @click="handleProcess(item)">处理</text>
|
||||
<view class="divider"></view>
|
||||
<text class="btn-link danger" @click="handleDelete(item)">删除</text>
|
||||
<text class="btn-link" v-if="item.status == 0" @click="handleProcess(item)">处理</text>
|
||||
<text class="btn-link gray" v-else>已回复</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="pagination-bar">
|
||||
<text class="page-total">共 {{ feedbackList.length }} 条</text>
|
||||
<text class="page-total">共 {{ total }} 条</text>
|
||||
<view class="page-nav">
|
||||
<view class="nav-btn"><text class="nav-icon"> < </text></view>
|
||||
<view class="nav-item active"><text class="nav-num">1</text></view>
|
||||
<view class="nav-btn"><text class="nav-icon"> > </text></view>
|
||||
<view class="nav-btn" :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-btn" :class="{ disabled: page >= totalPages }" @click="nextPage"><text class="nav-icon"> > </text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -68,42 +75,80 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface FeedbackItem {
|
||||
id: string
|
||||
nickname: string
|
||||
phone: string
|
||||
content: string
|
||||
status: string
|
||||
time: string
|
||||
}
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { fetchFeedbackPage, processFeedback, type KefuFeedback } from '@/services/admin/kefuService.uts'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const statusValue = ref('all')
|
||||
const feedbackList = ref<KefuFeedback[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const loading = ref(false)
|
||||
|
||||
const feedbackList = ref<FeedbackItem[]>([
|
||||
{ id: '37', nickname: 'hhh', phone: '14779580008', content: 'hhh', status: '未处理', time: '2025-12-20 03:22:10' },
|
||||
{ id: '36', nickname: 'hhh', phone: '14779580008', content: 'hhh', status: '未处理', time: '2025-12-20 03:22:05' },
|
||||
{ id: '35', nickname: '发给大哥他', phone: '13212324567', content: '回复挂号费', status: '未处理', time: '2025-12-17 22:11:35' },
|
||||
{ id: '34', nickname: '郑立民', phone: '15604580931', content: '我提现为啥不到账', status: '未处理', time: '2025-11-24 12:29:02' },
|
||||
{ id: '33', nickname: '11', phone: '15012760793', content: '22222', status: '未处理', time: '2025-10-16 17:55:31' },
|
||||
{ id: '32', nickname: '玉兔', phone: '13133164548', content: '我去会吐', status: '未处理', time: '2025-09-09 15:31:45' },
|
||||
{ id: '31', nickname: '哟', phone: '15151424728', content: '玩意', status: '未处理', time: '2025-09-04 15:40:28' },
|
||||
{ id: '30', nickname: '哟', phone: '15151424728', content: '玩意', status: '未处理', time: '2025-09-04 15:40:27' },
|
||||
{ id: '29', nickname: 'pww', phone: '15274289992', content: '我们的', status: '未处理', time: '2025-08-13 18:21:35' },
|
||||
{ id: '28', nickname: '1', phone: '18888888888', content: '1', status: '未处理', time: '2024-12-12 11:56:48' }
|
||||
])
|
||||
const statusOptions = [
|
||||
{ value: 'all', text: '全部状态' },
|
||||
{ value: '0', text: '未处理' },
|
||||
{ value: '1', text: '已处理' }
|
||||
]
|
||||
|
||||
const handleQuery = () => {
|
||||
console.log('查询:', searchQuery.value)
|
||||
const totalPages = computed((): number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
return Math.ceil(total.value / pageSize.value)
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const status = statusValue.value == 'all' ? null : parseInt(statusValue.value)
|
||||
const res = await fetchFeedbackPage(page.value, pageSize.value, searchQuery.value || null, status)
|
||||
feedbackList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载留言失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleProcess = (item: FeedbackItem) => {
|
||||
console.log('处理留言:', item.id)
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
function handleQuery() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleDelete = (item: FeedbackItem) => {
|
||||
console.log('删除留言:', item.id)
|
||||
async function handleProcess(item: KefuFeedback) {
|
||||
uni.showModal({
|
||||
title: '留言处理',
|
||||
editable: true,
|
||||
placeholderText: '请输入回复内容',
|
||||
success: async (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const ok = await processFeedback(item.id, res.content)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '处理成功' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -120,7 +165,6 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 过滤栏 */
|
||||
.filter-card {
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
@@ -128,7 +172,7 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 30px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
@@ -143,36 +187,8 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.date-picker-mock {
|
||||
width: 320px;
|
||||
height: 38px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: 13px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.sep {
|
||||
margin: 0 8px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.calendar-icon {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 320px;
|
||||
height: 38px;
|
||||
height: 36px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
@@ -222,6 +238,11 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
@@ -245,6 +266,13 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.u-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.u-account { font-size: 11px; color: #999; margin-top: 2px; }
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -252,23 +280,20 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
}
|
||||
|
||||
/* 列宽定义 */
|
||||
.col-id { width: 80px; justify-content: center; }
|
||||
.col-nick { width: 140px; }
|
||||
.col-phone { width: 180px; }
|
||||
.col-id { width: 60px; justify-content: center; }
|
||||
.col-nick { width: 160px; }
|
||||
.col-phone { width: 140px; }
|
||||
.col-content { flex: 1; justify-content: flex-start; }
|
||||
.col-status { width: 120px; justify-content: center; }
|
||||
.col-time { width: 220px; justify-content: center; }
|
||||
.col-op {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
justify-content: center;
|
||||
}
|
||||
.col-status { width: 100px; justify-content: center; }
|
||||
.col-time { width: 160px; justify-content: center; }
|
||||
.col-op { width: 100px; justify-content: center; }
|
||||
|
||||
.status-tag {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
&.processed { color: #52c41a; background: #f6ffed; border: 1px solid #b7eb8f; }
|
||||
&.pending { color: #fa8c16; background: #fff7e6; border: 1px solid #ffd591; }
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
@@ -276,17 +301,7 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-link.danger {
|
||||
color: #2d8cf0; /* 根据截图,删除也是蓝色的链接感,但通常后台会区分,这里按截图感肉眼识别为蓝色 */
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background-color: #e8eaec;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.btn-link.gray { color: #999; cursor: default; }
|
||||
|
||||
/* 分页栏 */
|
||||
.pagination-bar {
|
||||
@@ -318,6 +333,7 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
margin: 0 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@@ -344,4 +360,6 @@ const handleDelete = (item: FeedbackItem) => {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.disabled { cursor: not-allowed; opacity: 0.5; }
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<!-- 数据表格 -->
|
||||
<view class="table-container border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th col-id"><text class="th-txt">ID</text></view>
|
||||
<view class="th col-id"><text class="th-txt">序号</text></view>
|
||||
<view class="th col-avatar"><text class="th-txt">客服头像</text></view>
|
||||
<view class="th col-name"><text class="th-txt">客服名称</text></view>
|
||||
<view class="th col-status"><text class="th-txt">客服状态</text></view>
|
||||
@@ -19,25 +19,39 @@
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="item in kefuList" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="kefuList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无客服账号</text>
|
||||
</view>
|
||||
<view v-else class="table-row" v-for="(item, index) in kefuList" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td col-avatar">
|
||||
<image class="kefu-avatar" :src="item.avatar" mode="aspectFill"></image>
|
||||
<image v-if="item.avatar" class="kefu-avatar" :src="item.avatar" mode="aspectFill"></image>
|
||||
<view v-else class="kefu-avatar-placeholder">
|
||||
<text class="p-txt">U</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-name">
|
||||
<view class="u-info">
|
||||
<text class="td-txt">{{ item.nickname }}</text>
|
||||
<text class="u-account">账号: {{ item.user_account }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-name"><text class="td-txt">{{ item.name }}</text></view>
|
||||
<view class="td col-status">
|
||||
<view :class="['switch-btn', item.status ? 'switch-on' : 'switch-off']" @click="toggleStatus(item)">
|
||||
<view :class="['switch-btn', item.status == 1 ? 'switch-on' : 'switch-off']" @click="toggleStatus(item)">
|
||||
<view class="switch-inner">
|
||||
<text class="switch-txt">{{ item.status ? '开启' : '关闭' }}</text>
|
||||
<text class="switch-txt">{{ item.status == 1 ? '开启' : '关闭' }}</text>
|
||||
</view>
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.created_at.substring(0, 16).replace('T', ' ') }}</text></view>
|
||||
<view class="td col-op">
|
||||
<text class="btn-action">编辑</text>
|
||||
<text class="btn-action" @click="handleEdit(item)">编辑</text>
|
||||
<view class="divider"></view>
|
||||
<text class="btn-action">删除</text>
|
||||
<text class="btn-action danger" @click="handleDelete(item)">删除</text>
|
||||
<view class="divider"></view>
|
||||
<text class="btn-action">进入工作台</text>
|
||||
</view>
|
||||
@@ -46,19 +60,15 @@
|
||||
|
||||
<!-- 分页栏 -->
|
||||
<view class="pagination-bar">
|
||||
<text class="page-total">共 {{ kefuList.length }} 条</text>
|
||||
<view class="page-size-select">
|
||||
<text class="size-txt">15条/页</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
<text class="page-total">共 {{ total }} 条</text>
|
||||
<view class="page-nav">
|
||||
<view class="nav-prev"><text class="nav-icon"> < </text></view>
|
||||
<view class="nav-item active"><text class="nav-num">1</text></view>
|
||||
<view class="nav-next"><text class="nav-icon"> > </text></view>
|
||||
<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 class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" value="1" />
|
||||
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -67,32 +77,109 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { fetchKefuAccountPage, setKefuAccountStatus, deleteKefuAccount, type KefuAccount } from '@/services/admin/kefuService.uts'
|
||||
|
||||
interface KefuItem {
|
||||
id: string
|
||||
avatar: string
|
||||
name: string
|
||||
status: boolean
|
||||
time: string
|
||||
const kefuList = ref<KefuAccount[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const jumpPage = ref('')
|
||||
|
||||
const totalPages = computed((): number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
return Math.ceil(total.value / pageSize.value)
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchKefuAccountPage(
|
||||
page.value,
|
||||
pageSize.value,
|
||||
searchQuery.value || null
|
||||
)
|
||||
kefuList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载客服列表失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const kefuList = ref<KefuItem[]>([
|
||||
{ id: '167', avatar: '/static/logo.png', name: '客服', status: true, time: '2025-12-31 11:24:35' },
|
||||
{ id: '166', avatar: '/static/logo.png', name: '测试', status: true, time: '2025-11-18 17:33:26' },
|
||||
{ id: '165', avatar: '/static/logo.png', name: 'zhicheng', status: true, time: '2025-09-10 10:13:00' },
|
||||
{ id: '164', avatar: '/static/logo.png', name: 'kefu', status: true, time: '2025-07-29 11:50:20' },
|
||||
{ id: '162', avatar: '/static/logo.png', name: '呐呐', status: true, time: '2024-12-09 10:51:11' },
|
||||
{ id: '153', avatar: '/static/logo.png', name: '小天', status: true, time: '2024-09-19 16:04:59' },
|
||||
{ id: '152', avatar: '/static/logo.png', name: 'kk', status: true, time: '2024-09-16 00:05:22' }
|
||||
])
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const toggleStatus = (item: KefuItem) => {
|
||||
item.status = !item.status
|
||||
const handleQuery = () => {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const toggleStatus = async (item: KefuAccount) => {
|
||||
const targetStatus = item.status == 1 ? 0 : 1
|
||||
const ok = await setKefuAccountStatus(item.id, targetStatus)
|
||||
if (ok) {
|
||||
item.status = targetStatus
|
||||
uni.showToast({ title: '状态更新成功' })
|
||||
} else {
|
||||
uni.showToast({ title: '状态更新失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
console.log('Add Kefu')
|
||||
uni.showToast({ title: '添加客服功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
const handleEdit = (item: KefuAccount) => {
|
||||
console.log('Edit Kefu', item.id)
|
||||
uni.showToast({ title: '编辑功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
const handleDelete = (item: KefuAccount) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定要删除客服 "${item.nickname}" 吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await deleteKefuAccount(item.id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
} else {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
const goToJumpPage = () => {
|
||||
const target = parseInt(jumpPage.value)
|
||||
if (!isNaN(target) && target >= 1 && target <= totalPages.value) {
|
||||
page.value = target
|
||||
loadData()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '请输入有效的页码', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -157,6 +244,11 @@ const handleAdd = () => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
@@ -181,7 +273,7 @@ const handleAdd = () => {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 列宽百分比对齐截图 */
|
||||
/* 列宽百分比对齐 */
|
||||
.col-id { width: 80px; }
|
||||
.col-avatar { width: 120px; }
|
||||
.col-name { flex: 1; justify-content: flex-start; }
|
||||
@@ -202,6 +294,23 @@ const handleAdd = () => {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.kefu-avatar-placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
background-color: #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.p-txt { font-size: 14px; color: #999; }
|
||||
}
|
||||
|
||||
.u-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.u-account { font-size: 11px; color: #999; margin-top: 2px; }
|
||||
}
|
||||
|
||||
/* 开关组件 1:1 复刻 */
|
||||
.switch-btn {
|
||||
width: 60px;
|
||||
@@ -236,10 +345,11 @@ const handleAdd = () => {
|
||||
border-radius: 9px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.switch-on .switch-dot { right: 3px; }
|
||||
.switch-off .switch-dot { left: 3px; }
|
||||
.switch-on .switch-dot { transform: translateX(33px); }
|
||||
.switch-off .switch-dot { transform: translateX(0); }
|
||||
|
||||
/* 操作按钮 */
|
||||
.btn-action {
|
||||
@@ -248,6 +358,8 @@ const handleAdd = () => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.danger { color: #f5222d; }
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
@@ -266,21 +378,8 @@ const handleAdd = () => {
|
||||
|
||||
.page-total { font-size: 13px; color: #606266; margin-right: 15px; }
|
||||
|
||||
.page-size-select {
|
||||
border: 1px solid #dcdee2;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.size-txt { font-size: 13px; color: #606266; margin-right: 8px; }
|
||||
.arrow-down { font-size: 10px; color: #808695; }
|
||||
|
||||
.page-nav { display: flex; flex-direction: row; align-items: center; margin-right: 15px; }
|
||||
.nav-prev, .nav-next {
|
||||
.nav-prev, .nav-next, .nav-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdee2;
|
||||
@@ -289,20 +388,10 @@ const handleAdd = () => {
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
margin: 0 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdee2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.active {
|
||||
.nav-item.active {
|
||||
background-color: #2d8cf0;
|
||||
border-color: #2d8cf0;
|
||||
}
|
||||
@@ -310,6 +399,8 @@ const handleAdd = () => {
|
||||
.active .nav-num { color: #fff; }
|
||||
.nav-num, .nav-icon { font-size: 13px; color: #606266; }
|
||||
|
||||
.disabled { cursor: not-allowed; opacity: 0.5; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; }
|
||||
.jump-txt { font-size: 13px; color: #606266; }
|
||||
.jump-input {
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
>
|
||||
<view class="folder-icon">📂</view>
|
||||
<text class="cat-name">{{ cat.name }}</text>
|
||||
<view class="cat-ops" v-if="cat.id != 'all'">
|
||||
<text class="cat-op-btn" @click.stop="handleEditCategory(cat)">✎</text>
|
||||
<text class="cat-op-btn danger" @click.stop="handleDeleteCategory(cat)">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -30,56 +34,41 @@
|
||||
|
||||
<view class="table-card border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th col-id"><text class="th-txt">ID</text></view>
|
||||
<view class="th col-id"><text class="th-txt">序号</text></view>
|
||||
<view class="th col-cat"><text class="th-txt">分类</text></view>
|
||||
<view class="th col-title"><text class="th-txt">标题</text></view>
|
||||
<view class="th col-detail"><text class="th-txt">详情</text></view>
|
||||
<view class="th col-sort"><text class="th-txt">排序</text></view>
|
||||
<view class="th col-time"><text class="th-txt">添加时间</text></view>
|
||||
<view class="th col-op"><text class="th-txt">操作</text></view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="item in wordList" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view class="td col-cat"><text class="td-txt">{{ item.category }}</text></view>
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="wordList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>该分类下暂无话术</text>
|
||||
</view>
|
||||
<view v-else class="table-row" v-for="(item, index) in wordList" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ index + 1 }}</text></view>
|
||||
<view class="td col-cat"><text class="td-txt">{{ item.category_name }}</text></view>
|
||||
<view class="td col-title"><text class="td-txt">{{ item.title }}</text></view>
|
||||
<view class="td col-detail">
|
||||
<text class="td-txt ellipsis-2">{{ item.content }}</text>
|
||||
</view>
|
||||
<view class="td col-sort"><text class="td-txt">{{ item.sort }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></view>
|
||||
<view class="td col-op">
|
||||
<text class="btn-action" @click="handleEdit(item)">编辑</text>
|
||||
<text class="btn-action" @click="handleEditWord(item)">编辑</text>
|
||||
<view class="divider"></view>
|
||||
<text class="btn-action danger">删除</text>
|
||||
<text class="btn-action danger" @click="handleDeleteWord(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="pagination-bar">
|
||||
<text class="page-total">共 {{ wordList.length }} 条</text>
|
||||
<view class="page-size-select">
|
||||
<text class="size-txt">15条/页</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
<view class="page-nav">
|
||||
<view class="nav-prev"><text class="nav-icon"> < </text></view>
|
||||
<view class="nav-item active"><text class="nav-num">1</text></view>
|
||||
<view class="nav-next"><text class="nav-icon"> > </text></view>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" value="1" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 侧边弹窗 (Drawer) -->
|
||||
<!-- 话术编辑弹窗 (Drawer) -->
|
||||
<view v-if="showDrawer" class="drawer-mask" @click="closeDrawer">
|
||||
<view class="drawer-content" @click.stop>
|
||||
<view class="drawer-header">
|
||||
@@ -93,10 +82,12 @@
|
||||
<text class="label-txt">话术分类:</text>
|
||||
</view>
|
||||
<view class="input-box">
|
||||
<view class="select-mock">
|
||||
<text class="select-val">{{ formData.category || '请选择分类' }}</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
<picker :value="categoryIndex" :range="categoryOptions" range-key="label" @change="onCategoryChange">
|
||||
<view class="select-mock">
|
||||
<text class="select-val">{{ categoryOptions[categoryIndex]?.label || '请选择分类' }}</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -106,7 +97,7 @@
|
||||
<text class="label-txt">话术标题:</text>
|
||||
</view>
|
||||
<view class="input-box">
|
||||
<input class="input-base" v-model="formData.title" placeholder="请输入话术标题" />
|
||||
<input class="input-base" v-model="wordForm.title" placeholder="请输入话术标题" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -116,7 +107,7 @@
|
||||
<text class="label-txt">话术内容:</text>
|
||||
</view>
|
||||
<view class="input-box">
|
||||
<textarea class="textarea-base" v-model="formData.content" placeholder="请输入话术内容" />
|
||||
<textarea class="textarea-base" v-model="wordForm.content" placeholder="请输入话术内容" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -125,7 +116,7 @@
|
||||
<text class="label-txt">排序:</text>
|
||||
</view>
|
||||
<view class="input-box">
|
||||
<input class="input-base" type="number" v-model="formData.sort" />
|
||||
<input class="input-base" type="number" v-model="wordForm.sort" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -134,7 +125,7 @@
|
||||
<view class="btn-cancel" @click="closeDrawer">
|
||||
<text class="cancel-txt">取消</text>
|
||||
</view>
|
||||
<view class="btn-confirm" @click="submitForm">
|
||||
<view class="btn-confirm" @click="submitWordForm">
|
||||
<text class="confirm-txt">确定</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -144,85 +135,202 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import {
|
||||
fetchWordCategoryList,
|
||||
saveWordCategory,
|
||||
deleteWordCategory,
|
||||
fetchKefuWordList,
|
||||
saveKefuWord,
|
||||
deleteKefuWord,
|
||||
type WordCategory,
|
||||
type KefuWord
|
||||
} from '@/services/admin/kefuService.uts'
|
||||
|
||||
interface WordItem {
|
||||
id: string
|
||||
category: string
|
||||
title: string
|
||||
content: string
|
||||
sort: number
|
||||
time: string
|
||||
}
|
||||
const categories = ref<WordCategory[]>([])
|
||||
const wordList = ref<KefuWord[]>([])
|
||||
const loading = ref(false)
|
||||
const activeCategoryId = ref<string>('all')
|
||||
|
||||
interface Category {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const categories = ref<Category[]>([
|
||||
{ id: 1, name: '全部' },
|
||||
{ id: 2, name: '系统话术' }
|
||||
])
|
||||
|
||||
const activeCategoryId = ref(1)
|
||||
|
||||
const wordList = ref<WordItem[]>([
|
||||
{
|
||||
id: '67',
|
||||
category: '系统默认',
|
||||
title: '旗舰版介绍',
|
||||
content: '【旗舰版】可以授权给公司或者个人,企业自用搭建,不限制授权域名,提供专属技术总监、产品总监等。',
|
||||
sort: 0,
|
||||
time: '2024-09-27 10:15:07'
|
||||
}
|
||||
])
|
||||
|
||||
// 表单逻辑
|
||||
// 表单控制
|
||||
const showDrawer = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formData = reactive({
|
||||
const wordForm = reactive({
|
||||
id: '',
|
||||
category: '系统话术',
|
||||
categoryId: '',
|
||||
title: '',
|
||||
content: '',
|
||||
sort: 0
|
||||
})
|
||||
|
||||
const selectCategory = (id: number) => {
|
||||
const categoryOptions = computed(() : any[] => {
|
||||
return categories.value.map(c => ({ label: c.name, value: c.id }))
|
||||
})
|
||||
const categoryIndex = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
loadCategories()
|
||||
})
|
||||
|
||||
async function loadCategories() {
|
||||
const res = await fetchWordCategoryList()
|
||||
categories.value = [{ id: 'all', name: '全部' } as WordCategory, ...res]
|
||||
if (activeCategoryId.value == 'all') {
|
||||
loadWords(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWords(catId : string | null) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchKefuWordList(catId)
|
||||
wordList.value = res
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载话术失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectCategory(id : string) {
|
||||
activeCategoryId.value = id
|
||||
loadWords(id == 'all' ? null : id)
|
||||
}
|
||||
|
||||
const handleAddCategory = () => {
|
||||
console.log('添加分类')
|
||||
// 分类操作
|
||||
async function handleAddCategory() {
|
||||
uni.showModal({
|
||||
title: '添加分类',
|
||||
editable: true,
|
||||
placeholderText: '请输入分类名称',
|
||||
success: async (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const id = await saveWordCategory(null, res.content, 0)
|
||||
if (id != null) {
|
||||
uni.showToast({ title: '添加成功' })
|
||||
loadCategories()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleAddWord = () => {
|
||||
async function handleEditCategory(cat : WordCategory) {
|
||||
uni.showModal({
|
||||
title: '编辑分类',
|
||||
editable: true,
|
||||
content: cat.name,
|
||||
success: async (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const id = await saveWordCategory(cat.id, res.content, cat.sort)
|
||||
if (id != null) {
|
||||
uni.showToast({ title: '修改成功' })
|
||||
loadCategories()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDeleteCategory(cat : WordCategory) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定删除分类 "${cat.name}" 及其下所有话术吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await deleteWordCategory(cat.id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
if (activeCategoryId.value == cat.id) {
|
||||
activeCategoryId.value = 'all'
|
||||
}
|
||||
loadCategories()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 话术操作
|
||||
function handleAddWord() {
|
||||
isEdit.value = false
|
||||
formData.id = ''
|
||||
formData.title = ''
|
||||
formData.content = ''
|
||||
formData.sort = 0
|
||||
wordForm.id = ''
|
||||
wordForm.categoryId = activeCategoryId.value == 'all' ? '' : activeCategoryId.value
|
||||
wordForm.title = ''
|
||||
wordForm.content = ''
|
||||
wordForm.sort = 0
|
||||
|
||||
// 更新 picker 索引
|
||||
const idx = categoryOptions.value.findIndex((opt : any) => opt.value == wordForm.categoryId)
|
||||
categoryIndex.value = idx > -1 ? idx : 0
|
||||
|
||||
showDrawer.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (item: WordItem) => {
|
||||
function handleEditWord(item : KefuWord) {
|
||||
isEdit.value = true
|
||||
formData.id = item.id
|
||||
formData.category = item.category
|
||||
formData.title = item.title
|
||||
formData.content = item.content
|
||||
formData.sort = item.sort
|
||||
wordForm.id = item.id
|
||||
wordForm.categoryId = item.category_id
|
||||
wordForm.title = item.title
|
||||
wordForm.content = item.content
|
||||
wordForm.sort = item.sort
|
||||
|
||||
const idx = categoryOptions.value.findIndex((opt : any) => opt.value == item.category_id)
|
||||
categoryIndex.value = idx > -1 ? idx : 0
|
||||
|
||||
showDrawer.value = true
|
||||
}
|
||||
|
||||
const closeDrawer = () => {
|
||||
async function handleDeleteWord(item : KefuWord) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定删除该话术吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await deleteKefuWord(item.id)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadWords(activeCategoryId.value == 'all' ? null : activeCategoryId.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onCategoryChange(e : any) {
|
||||
categoryIndex.value = e.detail.value as number
|
||||
wordForm.categoryId = categoryOptions.value[categoryIndex.value].value as string
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
showDrawer.value = false
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
console.log('提交表单', formData)
|
||||
showDrawer.value = false
|
||||
async function submitWordForm() {
|
||||
if (!wordForm.categoryId) {
|
||||
uni.showToast({ title: '请选择分类', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!wordForm.title || !wordForm.content) {
|
||||
uni.showToast({ title: '请完善话术内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const resId = await saveKefuWord(
|
||||
isEdit.value ? wordForm.id : null,
|
||||
wordForm.categoryId,
|
||||
wordForm.title,
|
||||
wordForm.content,
|
||||
wordForm.sort
|
||||
)
|
||||
|
||||
if (resId != null) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
showDrawer.value = false
|
||||
loadWords(activeCategoryId.value == 'all' ? null : activeCategoryId.value)
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -248,9 +356,10 @@ const submitForm = () => {
|
||||
|
||||
/* 左侧分类 */
|
||||
.category-sidebar {
|
||||
width: 280px;
|
||||
width: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
@@ -261,17 +370,19 @@ const submitForm = () => {
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.plus-icon {
|
||||
font-size: 18px;
|
||||
color: #c0c4cc;
|
||||
color: #2d8cf0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.header-txt {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
color: #2d8cf0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
@@ -284,8 +395,9 @@ const submitForm = () => {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
padding: 0 15px 0 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category-item:hover {
|
||||
@@ -294,7 +406,8 @@ const submitForm = () => {
|
||||
|
||||
.category-item.active {
|
||||
background-color: #e6f7ff;
|
||||
border-right: 2px solid #2d8cf0;
|
||||
color: #2d8cf0;
|
||||
border-right: 3px solid #2d8cf0;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
@@ -304,9 +417,18 @@ const submitForm = () => {
|
||||
|
||||
.cat-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cat-ops {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.cat-op-btn { font-size: 16px; padding: 0 4px; }
|
||||
.cat-op-btn.danger { color: #f5222d; }
|
||||
|
||||
/* 右侧内容 */
|
||||
.content-main {
|
||||
flex: 1;
|
||||
@@ -339,6 +461,7 @@ const submitForm = () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@@ -365,10 +488,11 @@ const submitForm = () => {
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
height: 70px;
|
||||
min-height: 70px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -379,12 +503,13 @@ const submitForm = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.td-txt {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ellipsis-2 {
|
||||
@@ -401,8 +526,7 @@ const submitForm = () => {
|
||||
.col-title { width: 150px; }
|
||||
.col-detail { flex: 1; justify-content: flex-start; }
|
||||
.col-sort { width: 80px; }
|
||||
.col-time { width: 180px; }
|
||||
.col-op { width: 150px; }
|
||||
.col-op { width: 120px; }
|
||||
|
||||
.btn-action {
|
||||
font-size: 13px;
|
||||
@@ -410,9 +534,7 @@ const submitForm = () => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #ed4014;
|
||||
}
|
||||
.danger { color: #f5222d; }
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
@@ -421,90 +543,22 @@ const submitForm = () => {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination-bar {
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-total { font-size: 13px; color: #606266; margin-right: 15px; }
|
||||
|
||||
.page-size-select {
|
||||
border: 1px solid #dcdee2;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.size-txt { font-size: 13px; color: #606266; margin-right: 8px; }
|
||||
.nav-icon, .arrow-down { font-size: 10px; color: #808695; }
|
||||
|
||||
.page-nav { display: flex; flex-direction: row; align-items: center; margin-right: 15px; }
|
||||
.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;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: #2d8cf0;
|
||||
border-color: #2d8cf0;
|
||||
}
|
||||
|
||||
.active .nav-num { color: #fff; }
|
||||
.nav-num { font-size: 13px; color: #606266; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; }
|
||||
.jump-txt { font-size: 13px; color: #606266; }
|
||||
.jump-input {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
margin: 0 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 抽屉 Drawer 1:1 复刻 - 修正位置到右侧 */
|
||||
/* 抽屉 Drawer */
|
||||
.drawer-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0; /* 强制靠右占据右边屏幕 */
|
||||
width: 50%; /* 占屏幕一半宽度 */
|
||||
height: 100%;
|
||||
top: 0; right: 0;
|
||||
width: 450px; height: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
@@ -517,22 +571,10 @@ const submitForm = () => {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.drawer-title { font-size: 16px; font-weight: 600; color: #333; }
|
||||
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
|
||||
|
||||
.close-btn {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
}
|
||||
.drawer-body { flex: 1; padding: 24px; }
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
@@ -541,9 +583,7 @@ const submitForm = () => {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.align-start { align-items: flex-start; }
|
||||
|
||||
.label-box {
|
||||
width: 100px;
|
||||
@@ -553,19 +593,10 @@ const submitForm = () => {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ed4014;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.required { color: #ed4014; margin-right: 4px; }
|
||||
.label-txt { font-size: 14px; color: #606266; }
|
||||
|
||||
.label-txt {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
flex: 1;
|
||||
}
|
||||
.input-box { flex: 1; }
|
||||
|
||||
.select-mock {
|
||||
height: 36px;
|
||||
@@ -579,10 +610,10 @@ const submitForm = () => {
|
||||
}
|
||||
|
||||
.select-val { font-size: 14px; color: #333; }
|
||||
.arrow-down { font-size: 10px; color: #999; }
|
||||
|
||||
.input-base {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
width: 100%; height: 36px;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
@@ -590,8 +621,7 @@ const submitForm = () => {
|
||||
}
|
||||
|
||||
.textarea-base {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
width: 100%; height: 150px;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
|
||||
Reference in New Issue
Block a user