ykt-wxapp/pages/message/common-phrases.vue
2026-01-23 16:42:50 +08:00

938 lines
21 KiB
Vue
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="common-phrases-page">
<view class="content-wrapper">
<!-- 左侧分类列表 -->
<view class="category-sidebar">
<scroll-view class="category-list" scroll-y>
<view
v-for="category in categories"
:key="category.id"
class="category-item"
:class="{ active: currentCategory === category.id }"
@click="switchCategory(category.id)"
@longpress="handleCategoryLongPress(category)"
>
<view class="category-content">
<text class="category-name">{{ category.name }}</text>
<view
v-if="isEditMode && category.deletable"
class="delete-badge"
@click.stop="deleteCategory(category)"
>
<text class="delete-icon">×</text>
</view>
</view>
</view>
<view class="add-category-item" @click="showAddCategoryDialog">
<text class="plus-icon">+</text>
</view>
</scroll-view>
</view>
<!-- 右侧常用语列表 -->
<view class="phrases-container">
<!-- 顶部操作栏 -->
<view class="action-bar">
<view class="add-phrase-btn" @click="showAddPhraseDialog">
<text class="add-icon">+</text>
<text class="add-text">添加快捷回复</text>
</view>
<view class="edit-btn" @click="toggleEditMode">
<text class="edit-text">{{ isEditMode ? "完成" : "编辑" }}</text>
</view>
</view>
<scroll-view class="phrases-list" scroll-y>
<view
v-for="phrase in currentPhrases"
:key="phrase.id"
class="phrase-item"
@click="handlePhraseClick(phrase)"
>
<view class="phrase-content">{{ phrase.content }}</view>
<view v-if="isEditMode" class="phrase-actions">
<view class="action-btn edit" @click.stop="editPhrase(phrase)">
<text>编辑</text>
</view>
<view
class="action-btn delete"
@click.stop="deletePhrase(phrase)"
>
<text>删除</text>
</view>
</view>
</view>
<view v-if="currentPhrases.length === 0" class="empty-state">
<text class="empty-text">暂无常用语</text>
<text class="empty-hint">点击上方按钮添加常用语</text>
</view>
</scroll-view>
</view>
</view>
<!-- 添加/编辑常用语弹窗 -->
<view v-if="showPhrasePopup" class="popup-mask" @click="closePopup">
<view class="popup-content" @click.stop>
<view class="popup-header">
<text class="popup-title">{{
editingPhrase ? "编辑快捷回复" : "添加快捷回复"
}}</text>
<view class="popup-close" @click="closePopup">
<text class="close-icon">×</text>
</view>
</view>
<textarea
v-model="phraseForm.content"
class="phrase-textarea"
placeholder="在此处输入文字"
maxlength="500"
:auto-height="true"
></textarea>
<view class="char-count">{{ phraseForm.content.length }}/500</view>
<view class="popup-actions">
<button class="cancel-btn" @click="closePopup">取消</button>
<button class="confirm-btn" @click="savePhrase">
{{ editingPhrase ? "保存" : "确认添加" }}
</button>
</view>
</view>
</view>
<!-- 添加分类弹窗 -->
<view
v-if="showCategoryPopup"
class="popup-mask"
@click="closeCategoryPopup"
>
<view class="popup-content category-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">新建分类</text>
<view class="popup-close" @click="closeCategoryPopup">
<text class="close-icon">×</text>
</view>
</view>
<input
v-model="categoryForm.name"
class="category-input"
placeholder="请输入分类名最多6个字"
maxlength="6"
/>
<view class="popup-actions">
<button class="cancel-btn" @click="closeCategoryPopup">取消</button>
<button class="confirm-btn" @click="saveCategory">确认添加</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import api from "@/utils/api";
// 获取系统状态栏高度
const statusBarHeight = ref(0);
// 编辑模式
const isEditMode = ref(false);
// 分类数据
const categories = ref([
{ id: "visit", name: "文字随访", deletable: false },
{ id: "voice", name: "语音随访", deletable: false },
{ id: "common", name: "常用回复", deletable: false },
]);
const currentCategory = ref("common");
// 常用语数据
const phrases = ref([]);
// 当前分类的常用语
const currentPhrases = computed(() => {
return phrases.value.filter((p) => p.categoryId === currentCategory.value);
});
// 弹窗显示状态
const showPhrasePopup = ref(false);
const showCategoryPopup = ref(false);
// 表单数据
const phraseForm = ref({
content: "",
});
const categoryForm = ref({
name: "",
});
const editingPhrase = ref(null);
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 切换编辑模式
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value;
};
// 切换分类
const switchCategory = (categoryId) => {
currentCategory.value = categoryId;
};
// 处理常用语点击
const handlePhraseClick = (phrase) => {
if (isEditMode.value) {
return;
}
// 返回到聊天页面并发送消息
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
if (prevPage) {
prevPage.$vm.sendCommonPhrase(phrase.content);
}
uni.navigateBack();
};
// 显示添加常用语弹窗
const showAddPhraseDialog = () => {
editingPhrase.value = null;
phraseForm.value.content = "";
showPhrasePopup.value = true;
};
// 编辑常用语
const editPhrase = (phrase) => {
editingPhrase.value = phrase;
phraseForm.value.content = phrase.content;
showPhrasePopup.value = true;
};
// 保存常用语
const savePhrase = async () => {
if (!phraseForm.value.content.trim()) {
uni.showToast({
title: '请输入内容',
icon: 'none'
});
return;
}
try {
const corpId = uni.getStorageSync('corpId');
const userId = uni.getStorageSync('userId');
if (!corpId || !userId) {
uni.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
const result = await api('saveCommonPhrase', {
id: editingPhrase.value?.id,
categoryId: editingPhrase.value?.categoryId || currentCategory.value,
content: phraseForm.value.content,
corpId,
userId
});
if (result.code === 200) {
if (editingPhrase.value) {
// 更新
const index = phrases.value.findIndex(p => p.id === editingPhrase.value.id);
if (index !== -1) {
phrases.value[index].content = phraseForm.value.content;
}
} else {
// 新增
if (result.data) {
phrases.value.push(result.data);
}
}
uni.showToast({
title: editingPhrase.value ? '保存成功' : '添加成功',
icon: 'success'
});
closePopup();
} else {
uni.showToast({
title: result.message || '操作失败',
icon: 'none'
});
}
} catch (error) {
console.error('保存常用语失败:', error);
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
};
// 删除常用语
const deletePhrase = (phrase) => {
uni.showModal({
title: "提示",
content: "确定删除该常用语吗?",
success: async (res) => {
if (res.confirm) {
try {
await api("deleteCommonPhrase", {
id: phrase.id,
corpId: uni.getStorageSync("corpId"),
});
const index = phrases.value.findIndex((p) => p.id === phrase.id);
if (index !== -1) {
phrases.value.splice(index, 1);
}
uni.showToast({
title: "删除成功",
icon: "success",
});
} catch (error) {
console.error("删除常用语失败:", error);
uni.showToast({
title: "删除失败",
icon: "none",
});
}
}
},
});
};
// 显示添加分类弹窗
const showAddCategoryDialog = () => {
categoryForm.value.name = "";
showCategoryPopup.value = true;
};
// 保存分类
const saveCategory = () => {
if (!categoryForm.value.name.trim()) {
uni.showToast({
title: "请输入分类名",
icon: "none",
});
return;
}
categories.value.push({
id: `category_${Date.now()}`,
name: categoryForm.value.name,
deletable: true, // 用户添加的分类可删除
});
uni.showToast({
title: "添加成功",
icon: "success",
});
closeCategoryPopup();
};
// 长按分类
const handleCategoryLongPress = (category) => {
if (!category.deletable) return;
uni.showModal({
title: "提示",
content: `确定删除分类"${category.name}"吗?该分类下的所有常用语也将被删除。`,
success: (res) => {
if (res.confirm) {
deleteCategory(category);
}
},
});
};
// 删除分类
const deleteCategory = (category) => {
if (!category.deletable) {
uni.showToast({
title: "默认分类不可删除",
icon: "none",
});
return;
}
// 删除分类下的所有常用语
phrases.value = phrases.value.filter((p) => p.categoryId !== category.id);
// 删除分类
const index = categories.value.findIndex((c) => c.id === category.id);
if (index !== -1) {
categories.value.splice(index, 1);
}
// 如果删除的是当前分类,切换到第一个分类
if (currentCategory.value === category.id && categories.value.length > 0) {
currentCategory.value = categories.value[0].id;
}
uni.showToast({
title: "删除成功",
icon: "success",
});
};
// 关闭弹窗
const closePopup = () => {
showPhrasePopup.value = false;
};
const closeCategoryPopup = () => {
showCategoryPopup.value = false;
};
// 加载常用语数据
const loadPhrases = async () => {
try {
const corpId = uni.getStorageSync('corpId');
const userId = uni.getStorageSync('userId');
if (!corpId) {
uni.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
const result = await api('getCommonPhrases', {
corpId,
userId
});
if (result.code === 200) {
// 更新常用语列表
if (Array.isArray(result.data)) {
phrases.value = result.data;
}
// 更新分类列表(合并默认分类和后台分类)
if (Array.isArray(result.categories)) {
const backendCategories = result.categories.map(cat => ({
id: cat.id,
name: cat.name,
deletable: true // 后台分类都可以删除
}));
// 如果后台有分类,使用后台分类,否则使用默认分类
if (backendCategories.length > 0) {
categories.value = backendCategories;
// 设置当前分类为第一个
if (!currentCategory.value || !categories.value.find(c => c.id === currentCategory.value)) {
currentCategory.value = categories.value[0].id;
}
}
}
}
} catch (error) {
console.error('加载常用语失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
}
};
// 模拟数据
const getMockPhrases = () => {
return [
{
id: 1,
categoryId: "common",
content:
"想要买草药自己熬还是配方颗粒直接开水冲着喝线上购买全国包邮一二线城市一般1-2天送到其他城市可能慢一些。",
},
{
id: 2,
categoryId: "common",
content: "以前得过什么病吗?做过什么检查吗?有检查结果吗?详细说一下。",
},
{
id: 3,
categoryId: "common",
content: "把我之前给您开的处方,拍个照上传给我看一下,拍招清楚一点。",
},
{
id: 4,
categoryId: "common",
content: "服药期间要慎炸身体,少吃油炸等辛辣食物,按时作息,精神放松。",
},
{
id: 5,
categoryId: "common",
content:
"煎药最好用砂锅避免用铁锅煎药前用冷水浸泡30分钟水量超过药材2-5厘米一般中药煎2次第1次煮沸后再煎20分钟就可以倒出来了再次加水煮沸后再煎30分钟后倒出将2次倒出的药混合在一起就可以按照医嘱用药了。",
},
{
id: 6,
categoryId: "common",
content:
"如果在线上买药,点击整体治疗方案,选择首选处方,划价取药,填写地址后去付款即可。",
},
{
id: 7,
categoryId: "voice",
content: "想要买草药自己熬还是配方颗粒直接开水冲着喝?",
},
{
id: 8,
categoryId: "visit",
content: "您好,我是您的健康管理师,请问有什么可以帮助您的吗?",
},
];
};
onMounted(() => {
// 获取系统信息
const systemInfo = uni.getSystemInfoSync();
statusBarHeight.value = systemInfo.statusBarHeight || 0;
loadPhrases();
});
</script>
<style scoped lang="scss">
// 使用项目主题色
$primary-color: #0877f1;
$primary-light: #e8f3ff;
$primary-gradient-start: #1b5cc8;
$primary-gradient-end: #0877f1;
.common-phrases-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
// 自定义导航栏
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background: linear-gradient(
135deg,
$primary-gradient-end 0%,
$primary-gradient-start 100%
);
z-index: 1000;
box-shadow: 0 2rpx 12rpx rgba(8, 119, 241, 0.15);
}
.navbar-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
position: relative;
}
.navbar-left {
width: 80rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: flex-start;
.back-icon {
font-size: 56rpx;
color: #fff;
font-weight: 300;
line-height: 1;
}
}
.navbar-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
.navbar-title {
font-size: 36rpx;
font-weight: 600;
color: #fff;
}
}
.navbar-right {
width: 80rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: flex-end;
.more-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 12rpx 16rpx;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 40rpx;
.dot {
font-size: 24rpx;
color: #fff;
line-height: 1;
}
}
}
// 标题栏
.action-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin: 24rpx 24rpx 0;
.edit-btn {
padding: 16rpx 32rpx;
.edit-text {
font-size: 28rpx;
color: $primary-color;
font-weight: 500;
}
}
.add-phrase-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 16rpx 24rpx;
background: linear-gradient(
135deg,
$primary-gradient-end 0%,
$primary-gradient-start 100%
);
border-radius: 40rpx;
box-shadow: 0 4rpx 16rpx rgba(8, 119, 241, 0.2);
.add-icon {
font-size: 32rpx;
color: #fff;
font-weight: 600;
margin-right: 8rpx;
}
.add-text {
font-size: 28rpx;
color: #fff;
font-weight: 500;
}
}
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
// 左侧分类栏
.category-sidebar {
width: 180rpx;
background-color: #fff;
border-right: 1px solid #f0f0f0;
.category-list {
height: 100%;
}
.category-item {
position: relative;
padding: 40rpx 16rpx;
text-align: center;
background-color: #fff;
transition: all 0.3s;
.category-content {
position: relative;
}
.category-name {
font-size: 28rpx;
color: #666;
transition: all 0.3s;
}
&.active {
background-color: $primary-light;
.category-name {
color: $primary-color;
font-weight: 600;
font-size: 30rpx;
}
}
.delete-badge {
position: absolute;
top: -12rpx;
right: 8rpx;
width: 32rpx;
height: 32rpx;
background-color: #ff3b30;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(255, 59, 48, 0.3);
.delete-icon {
font-size: 28rpx;
color: #fff;
line-height: 1;
font-weight: 300;
}
}
}
.add-category-item {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 16rpx;
font-size: 24rpx;
color: $primary-color;
background-color: #fff;
border-top: 1px solid #f0f0f0;
.plus-icon {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
font-weight: 300;
color: $primary-color;
background-color: $primary-light;
border-radius: 50%;
}
}
}
// 右侧内容区
.phrases-container {
flex: 1;
display: flex;
flex-direction: column;
background-color: #f8f9fa;
}
.phrases-list {
flex: 1;
padding: 24rpx;
height: 0; // 配合 flex: 1 实现固定高度
overflow-y: auto; // 支持垂直滚动
.phrase-item {
padding: 24rpx;
margin-bottom: 16rpx;
background-color: #fff;
border-radius: 16rpx;
border-left: 6rpx solid $primary-color;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.phrase-content {
font-size: 28rpx;
color: #333;
line-height: 1.8;
}
.phrase-actions {
display: flex;
justify-content: flex-end;
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1px solid #f0f0f0;
gap: 16rpx;
.action-btn {
padding: 10rpx 28rpx;
font-size: 24rpx;
border-radius: 40rpx;
transition: all 0.3s;
&.edit {
color: $primary-color;
background-color: $primary-light;
}
&.delete {
color: #ff3b30;
background-color: #ffe6e6;
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 0;
.empty-text {
font-size: 32rpx;
color: #999;
margin-bottom: 16rpx;
}
.empty-hint {
font-size: 24rpx;
color: #ccc;
}
}
}
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(4rpx);
}
.popup-content {
width: 600rpx;
padding: 40rpx;
background-color: #fff;
border-radius: 24rpx;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 50%;
.close-icon {
font-size: 40rpx;
color: #999;
line-height: 1;
font-weight: 300;
}
}
}
.phrase-textarea {
width: 100%;
min-height: 300rpx;
max-height: 500rpx;
padding: 20rpx;
font-size: 28rpx;
color: #333;
background-color: #f8f9fa;
border-radius: 12rpx;
border: 2rpx solid #e9ecef;
margin-bottom: 12rpx;
box-sizing: border-box;
line-height: 1.6;
}
.char-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-bottom: 32rpx;
}
.category-input {
width: 100%;
padding: 20rpx;
font-size: 28rpx;
color: #333;
background-color: #f8f9fa;
border-radius: 12rpx;
border: 2rpx solid #e9ecef;
margin-bottom: 32rpx;
box-sizing: border-box;
height: 100rpx;
}
.popup-actions {
display: flex;
justify-content: space-between;
gap: 20rpx;
button {
flex: 1;
padding: 10rpx;
font-size: 28rpx;
border-radius: 12rpx;
border: none;
font-weight: 500;
&.cancel-btn {
color: #666;
background-color: #f5f5f5;
}
&.confirm-btn {
color: #fff;
background: linear-gradient(
135deg,
$primary-gradient-end 0%,
$primary-gradient-start 100%
);
box-shadow: 0 4rpx 12rpx rgba(8, 119, 241, 0.3);
}
}
}
}
</style>