ykt-wxapp/pages/message/common-phrases.vue
2026-01-26 18:08:01 +08:00

1059 lines
24 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 allCategories"
:key="category.id"
class="category-item"
:class="{ active: currentCategory === category.id }"
@click="handleCategoryClick(category)"
@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>
</scroll-view>
<!-- 新增分类按钮固定在底部 -->
<view class="add-category-footer" @click="showAddCategoryDialog">
<text class="add-category-text">新增分类</text>
</view>
</view>
<!-- 右侧常用语列表 -->
<view class="phrases-container">
<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 class="footer-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>
</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">{{
editingCategory ? "编辑分类" : "新建分类"
}}</text>
<view class="popup-close" @click="closeCategoryPopup">
<text class="close-icon">×</text>
</view>
</view>
<input
v-model="categoryForm.name"
class="category-input"
placeholder="请输入分类名最多6个字"
/>
<view class="popup-actions">
<button class="cancel-btn" @click="closeCategoryPopup">取消</button>
<button class="confirm-btn" @click="saveCategory">
{{ editingCategory ? "保存" : "确认添加" }}
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { storeToRefs } from "pinia";
import api from "@/utils/api";
import useAccountStore from "@/store/account";
// 获取 store
const { doctorInfo } = storeToRefs(useAccountStore());
// 获取系统状态栏高度
const statusBarHeight = ref(0);
// 编辑模式
const isEditMode = ref(false);
// 默认分类(虚拟分类,不存在于数据库)
const DEFAULT_CATEGORY = {
id: "__default__",
name: "默认",
sort: -1,
deletable: false,
type: "personal",
};
// 分类数据(初始为空,从后台加载)
const categories = ref([]);
// 机构分类数据
const corpCategories = ref([]);
// 所有分类(包含默认分类 + 个人分类 + 机构分类)
const allCategories = computed(() => {
// 个人分类
const personalCats = [DEFAULT_CATEGORY, ...categories.value];
// 机构分类(标记为只读)
const corpCats = corpCategories.value.map(cat => ({
...cat,
type: "corp",
deletable: false,
isCorpCategory: true, // 标记为机构分类
}));
return [...personalCats, ...corpCats];
});
const currentCategory = ref(DEFAULT_CATEGORY.id);
// 常用语数据
const phrases = ref([]);
// 机构常用语数据
const corpPhrases = ref([]);
// 当前分类的常用语
const currentPhrases = computed(() => {
// 获取当前分类信息
const currentCat = allCategories.value.find(c => c.id === currentCategory.value);
if (currentCategory.value === DEFAULT_CATEGORY.id) {
// 默认分类显示没有 categoryId 或 categoryId 为空的个人常用语
return phrases.value.filter((p) => !p.categoryId);
}
// 如果是机构分类,返回机构常用语
if (currentCat?.isCorpCategory) {
return corpPhrases.value.filter((p) => p.categoryId === currentCategory.value);
}
// 个人分类,返回个人常用语
const filtered = phrases.value.filter((p) => p.categoryId === currentCategory.value);
console.log("当前分类:", currentCategory.value);
return filtered;
});
// 弹窗显示状态
const showPhrasePopup = ref(false);
const showCategoryPopup = ref(false);
// 表单数据
const phraseForm = ref({
content: "",
});
const categoryForm = ref({
name: "",
});
const editingPhrase = ref(null);
const editingCategory = ref(null);
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 切换编辑模式
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value;
};
// 切换分类
const switchCategory = (categoryId) => {
currentCategory.value = categoryId;
};
// 处理分类点击(编辑模式下编辑分类名称)
const handleCategoryClick = (category) => {
if (isEditMode.value && category.deletable) {
// 编辑模式下,点击可编辑的分类,弹出编辑框
editCategory(category);
} else {
// 非编辑模式,切换分类
switchCategory(category.id);
}
};
// 编辑分类
const editCategory = (category) => {
editingCategory.value = category;
categoryForm.value.name = category.name;
showCategoryPopup.value = true;
};
// 处理常用语点击
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 {
if (!doctorInfo.value) {
uni.showToast({
title: "请先登录",
icon: "none",
});
return;
}
const corpId = doctorInfo.value.corpId;
const userId = doctorInfo.value.userid;
// 如果是默认分类categoryId 设置为 null
let categoryId = editingPhrase.value?.categoryId || currentCategory.value;
if (categoryId === DEFAULT_CATEGORY.id) {
categoryId = null;
}
const result = await api("savePersonalPhrase", {
id: editingPhrase.value?.id,
categoryId: categoryId,
content: phraseForm.value.content,
corpId,
userId,
});
console.log("保存常用语返回结果:", result);
if (result.success) {
if (editingPhrase.value) {
// 更新
const index = phrases.value.findIndex(
(p) => p.id === editingPhrase.value.id
);
if (index !== -1) {
phrases.value[index].content = phraseForm.value.content;
// 如果是默认分类categoryId 设置为 null
const newCategoryId = editingPhrase.value.categoryId || currentCategory.value;
phrases.value[index].categoryId = newCategoryId === DEFAULT_CATEGORY.id ? null : newCategoryId;
}
} else {
// 新增
console.log("新增常用语,返回数据:", result.data);
console.log("当前分类ID:", currentCategory.value);
if (result.data) {
phrases.value.push(result.data);
console.log("添加后的 phrases:", phrases.value);
}
}
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 {
const corpId = doctorInfo.value.corpId;
const userId = doctorInfo.value.userid;
const result = await api("deletePersonalPhrase", {
id: phrase.id,
corpId,
userId,
});
if (result.success) {
const index = phrases.value.findIndex((p) => p.id === phrase.id);
if (index !== -1) {
phrases.value.splice(index, 1);
}
uni.showToast({
title: "删除成功",
icon: "success",
});
} else {
uni.showToast({
title: result.message || "删除失败",
icon: "none",
});
}
} catch (error) {
console.error("删除常用语失败:", error);
uni.showToast({
title: "删除失败",
icon: "none",
});
}
}
},
});
};
// 显示添加分类弹窗
const showAddCategoryDialog = () => {
editingCategory.value = null;
categoryForm.value.name = "";
showCategoryPopup.value = true;
};
// 保存分类
const saveCategory = async () => {
if (!categoryForm.value.name.trim()) {
uni.showToast({
title: "请输入分类名",
icon: "none",
});
return;
}
if (categoryForm.value.name.length > 6) {
uni.showToast({
title: "输入内容超过6个字",
icon: "none",
});
return;
}
try {
const corpId = doctorInfo.value.corpId;
const userId = doctorInfo.value.userid;
const result = await api("savePersonalPhraseCategory", {
id: editingCategory.value?.id, // 如果是编辑,传入 id
name: categoryForm.value.name,
corpId,
userId,
});
if (result.success && result.data) {
if (editingCategory.value) {
// 更新分类
const index = categories.value.findIndex(
(c) => c.id === editingCategory.value.id
);
if (index !== -1) {
categories.value[index].name = result.data.name;
}
uni.showToast({
title: "修改成功",
icon: "success",
});
} else {
// 新增分类
categories.value.push({
id: result.data.id,
name: result.data.name,
sort: result.data.sort,
deletable: result.data.deletable,
});
uni.showToast({
title: "添加成功",
icon: "success",
});
}
closeCategoryPopup();
} else {
uni.showToast({
title: result.message || "操作失败",
icon: "none",
});
}
} catch (error) {
console.error("保存分类失败:", error);
uni.showToast({
title: "操作失败",
icon: "none",
});
}
};
// 长按分类
const handleCategoryLongPress = (category) => {
if (!category.deletable) return;
uni.showModal({
title: "提示",
content: `确定删除分类"${category.name}"吗?该分类下的所有常用语也将被删除。`,
success: (res) => {
if (res.confirm) {
deleteCategory(category);
}
},
});
};
// 删除分类
const deleteCategory = async (category) => {
if (!category.deletable) {
uni.showToast({
title: "该分类不可删除",
icon: "none",
});
return;
}
try {
const corpId = doctorInfo.value.corpId;
const userId = doctorInfo.value.userid;
const result = await api("deletePersonalPhraseCategory", {
id: category.id,
corpId,
userId,
});
if (result.success) {
// 删除分类
const index = categories.value.findIndex((c) => c.id === category.id);
if (index !== -1) {
categories.value.splice(index, 1);
}
// 如果删除的是当前分类,切换到默认分类
if (currentCategory.value === category.id) {
currentCategory.value = DEFAULT_CATEGORY.id;
}
uni.showToast({
title: "删除成功",
icon: "success",
});
} else {
uni.showToast({
title: result.message || "删除失败",
icon: "none",
});
}
} catch (error) {
console.error("删除分类失败:", error);
uni.showToast({
title: "删除失败",
icon: "none",
});
}
};
// 关闭弹窗
const closePopup = () => {
showPhrasePopup.value = false;
};
const closeCategoryPopup = () => {
showCategoryPopup.value = false;
};
// 加载常用语数据
const loadPhrases = async () => {
try {
const corpId = doctorInfo.value.corpId;
const userId = doctorInfo.value.userid;
const result = await api("getPersonalPhrases", {
corpId,
userId,
});
if (result.success && result.data) {
// 更新常用语列表
if (Array.isArray(result.data.phrases)) {
phrases.value = result.data.phrases;
}
// 更新分类列表
if (Array.isArray(result.data.categories)) {
categories.value = result.data.categories;
// 如果当前分类不存在,保持默认分类
if (currentCategory.value && currentCategory.value !== DEFAULT_CATEGORY.id) {
const categoryExists = categories.value.find((c) => c.id === currentCategory.value);
if (!categoryExists) {
currentCategory.value = DEFAULT_CATEGORY.id;
}
}
}
} else {
uni.showToast({
title: result.message || "加载失败",
icon: "none",
});
}
} catch (error) {
console.error("加载常用语失败:", error);
uni.showToast({
title: "加载失败",
icon: "none",
});
}
};
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;
}
}
}
// 底部操作栏(页脚)
.footer-action-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 24rpx;
background-color: #fff;
border-top: 1px solid #f0f0f0;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
.add-phrase-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 24rpx;
background: linear-gradient(
135deg,
$primary-gradient-end 0%,
$primary-gradient-start 100%
);
border-radius: 30rpx;
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;
}
}
.edit-btn {
padding: 20rpx 32rpx;
// background-color: $primary-light;
border-radius: 20rpx;
// box-shadow: 0 2rpx 8rpx rgba(8, 119, 241, 0.15);
.edit-text {
font-size: 28rpx;
color: $primary-color;
font-weight: 500;
}
}
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
// 左侧分类栏
.category-sidebar {
width: 210rpx;
background-color: #fff;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
.category-list {
flex: 1;
height: 0; // 配合 flex: 1 实现固定高度
overflow-y: auto; // 支持垂直滚动
}
.category-item {
position: relative;
padding: 20rpx 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: -20rpx;
right: 2rpx;
width: 28rpx;
height: 28rpx;
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: 24rpx;
color: #fff;
line-height: 1;
font-weight: 300;
}
}
}
// 新增分类按钮(固定在底部)
.add-category-footer {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 16rpx;
background-color: #fff;
border-top: 1px solid #f0f0f0;
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
margin-bottom: 18rpx;
.add-category-text {
font-size: 26rpx;
color: $primary-color;
font-weight: 500;
}
&:active {
background-color: $primary-light;
}
}
}
// 右侧内容区
.phrases-container {
flex: 1;
display: flex;
flex-direction: column;
background-color: #f8f9fa;
}
.phrases-list {
flex: 1;
padding: 10rpx;
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: 0 20rpx; // 只设置左右 padding去掉上下 padding
font-size: 28rpx;
color: #333;
background-color: #f8f9fa;
border-radius: 12rpx;
border: 2rpx solid #e9ecef;
margin-bottom: 32rpx;
box-sizing: border-box;
height: 100rpx;
line-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>