ykt-wxapp/pages/message/common-phrases.vue

1398 lines
34 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="tabs-bar">
<view
class="tab-item"
:class="{ active: activeTab === 'mine' }"
@click="switchTab('mine')"
>
我的常用语
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'more' }"
@click="switchTab('more')"
>
更多常用语
</view>
</view>
<view class="content-wrapper">
<view class="category-sidebar">
<scroll-view class="category-list" scroll-y>
<view
v-for="category in currentCategories"
: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="activeTab === 'mine' && isEditMode && category.deletable"
class="delete-badge"
@click.stop="deleteCategory(category)"
>
<text class="delete-icon">×</text>
</view>
</view>
</view>
</scroll-view>
<view
v-if="activeTab === 'mine'"
class="add-category-footer"
@click="showAddCategoryDialog"
>
<text class="add-category-text">新增分类</text>
</view>
</view>
<view class="phrases-container">
<view class="list-header">
<view class="current-title">
{{ currentCategoryName }}{{ currentPhrases.length }}
</view>
<view class="search-box">
<text class="search-icon"></text>
<input
v-model="searchKeyword"
class="search-input"
placeholder="搜索常用语"
confirm-type="search"
/>
<text v-if="searchKeyword" class="clear-search" @click="searchKeyword = ''">×</text>
</view>
</view>
<scroll-view class="phrases-list" scroll-y>
<view
v-for="phrase in currentPhrases"
:key="phrase.id"
class="phrase-item"
>
<view class="phrase-top">
<view class="phrase-content">{{ phrase.content }}</view>
<view
v-if="activeTab === 'mine' && phrase.sourceCommonWordId"
class="collected-badge"
>
已收藏
</view>
</view>
<view v-if="phrase.files && phrase.files.length" class="phrase-images">
<image
v-for="(file, fileIndex) in phrase.files"
:key="file.url || fileIndex"
class="phrase-image"
:src="file.url"
mode="aspectFill"
@click.stop="previewFiles(phrase.files, fileIndex)"
/>
</view>
<view class="phrase-actions">
<view class="action-btn send" @click.stop="sendPhrase(phrase)">
<text class="action-icon"></text>
<text>发送</text>
</view>
<template v-if="activeTab === 'mine'">
<view class="action-btn edit" @click.stop="editPhrase(phrase)">
<text class="action-icon"></text>
<text>编辑</text>
</view>
<view class="action-btn delete" @click.stop="deletePhrase(phrase)">
<text class="action-icon"></text>
<text>删除</text>
</view>
</template>
<view
v-else
class="action-btn collect"
:class="{ collected: isFavorite(phrase) }"
@click.stop="toggleFavorite(phrase)"
>
<text class="action-icon">{{ isFavorite(phrase) ? "★" : "☆" }}</text>
<text>{{ isFavorite(phrase) ? "已收藏" : "收藏" }}</text>
</view>
</view>
</view>
<view v-if="currentPhrases.length === 0" class="empty-state">
<text class="empty-text">暂无常用语</text>
<text class="empty-hint">
{{ activeTab === "mine" ? "可添加快捷回复或收藏更多常用语" : "可在后台常用语库维护后使用" }}
</text>
</view>
<view v-if="activeTab === 'more' && currentPhrases.length > 0" class="collect-tip">
收藏时请选择保存到我的常用语的真实目录
</view>
</scroll-view>
<view v-if="activeTab === 'mine'" 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 phrase-popup" @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>
<view class="form-label">所在目录</view>
<picker
mode="selector"
:range="myCategories"
range-key="name"
:value="phraseCategoryIndex"
@change="handlePhraseCategoryChange"
>
<view class="category-picker">
<text>{{ selectedPhraseCategoryName }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
<view class="form-hint">可选择保存到指定目录</view>
<view class="form-label content-label">常用语内容</view>
<textarea
v-model="phraseForm.content"
class="phrase-textarea"
placeholder="请输入常用语内容,支持换行"
maxlength="500"
:auto-height="false"
></textarea>
<view class="char-count">{{ phraseForm.content.length }}/500</view>
<form-files
class="phrase-files-cell"
:form="phraseForm"
title="files"
name="图片附件"
:max="9"
@change="handlePhraseFilesChange"
/>
<view class="popup-actions">
<button class="cancel-btn" @click="closePopup">取消</button>
<button class="confirm-btn" @click="savePhrase">保存</button>
</view>
</view>
</view>
<view v-if="showCollectPopup" class="popup-mask" @click="closeCollectPopup">
<view class="popup-content collect-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">收藏常用语</text>
<view class="popup-close" @click="closeCollectPopup">
<text class="close-icon">×</text>
</view>
</view>
<view class="form-label">保存目录</view>
<picker
mode="selector"
:range="myCategories"
range-key="name"
:value="collectCategoryIndex"
@change="handleCollectCategoryChange"
>
<view class="category-picker">
<text>{{ selectedCollectCategoryName }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
<view class="form-hint">收藏后将添加到所选目录</view>
<view class="collect-preview">{{ collectingPhrase?.content || "" }}</view>
<view v-if="collectingPhrase?.files && collectingPhrase.files.length" class="phrase-images collect-images">
<image
v-for="(file, fileIndex) in collectingPhrase.files"
:key="file.url || fileIndex"
class="phrase-image"
:src="file.url"
mode="aspectFill"
@click.stop="previewFiles(collectingPhrase.files, fileIndex)"
/>
</view>
<view class="popup-actions">
<button class="cancel-btn" @click="closeCollectPopup">取消</button>
<button class="confirm-btn" @click="confirmFavorite">收藏</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="请输入分类名最多10个字"
/>
<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 { storeToRefs } from "pinia";
import api from "@/utils/api";
import useAccountStore from "@/store/account";
import formFiles from "@/components/form-template/form-cell/form-files.vue";
import { normalizeFileUrl } from "@/utils/file";
const { doctorInfo } = storeToRefs(useAccountStore());
const activeTab = ref("mine");
const isEditMode = ref(false);
const searchKeyword = ref("");
const currentCategory = ref("");
const myCategories = ref([]);
const myPhrases = ref([]);
const corpCategories = ref([]);
const corpPhrases = ref([]);
const showPhrasePopup = ref(false);
const showCategoryPopup = ref(false);
const showCollectPopup = ref(false);
const editingPhrase = ref(null);
const editingCategory = ref(null);
const collectingPhrase = ref(null);
const phraseForm = ref({
content: "",
cateId: "",
files: [],
});
const collectForm = ref({
cateId: "",
});
const categoryForm = ref({
name: "",
});
const currentCategories = computed(() => {
return activeTab.value === "more" ? corpCategories.value : myCategories.value;
});
const currentCategoryName = computed(() => {
const category = currentCategories.value.find((item) => item.id === currentCategory.value);
return category ? category.name : "未选择";
});
const firstMyCategory = computed(() => {
return myCategories.value[0];
});
const favoriteMap = computed(() => {
return myPhrases.value.reduce((map, item) => {
if (item.sourceCommonWordId) map[item.sourceCommonWordId] = item;
return map;
}, {});
});
const getCategoryScopeIds = (categories, categoryId) => {
const ids = [categoryId].filter(Boolean);
let changed = true;
while (changed) {
changed = false;
categories.forEach((item) => {
if (item.parentId && ids.includes(item.parentId) && !ids.includes(item.id)) {
ids.push(item.id);
changed = true;
}
});
}
return ids;
};
const currentPhrases = computed(() => {
const keyword = searchKeyword.value.trim();
const source = activeTab.value === "more" ? corpPhrases.value : myPhrases.value;
const scopeIds = getCategoryScopeIds(currentCategories.value, currentCategory.value);
let list = source.filter((item) => scopeIds.includes(item.cateId));
if (keyword) {
list = list.filter((item) => String(item.content || "").includes(keyword));
}
return list;
});
const phraseCategoryIndex = computed(() => {
const index = myCategories.value.findIndex((item) => item.id === phraseForm.value.cateId);
return index > -1 ? index : 0;
});
const selectedPhraseCategoryName = computed(() => {
return myCategories.value[phraseCategoryIndex.value]?.name || "请选择目录";
});
const collectCategoryIndex = computed(() => {
const index = myCategories.value.findIndex((item) => item.id === collectForm.value.cateId);
return index > -1 ? index : 0;
});
const selectedCollectCategoryName = computed(() => {
return myCategories.value[collectCategoryIndex.value]?.name || "请选择目录";
});
const normalizeCategory = (item, categoryType) => ({
id: item._id || item.id,
name: item.label || item.name || "未命名",
sort: item.sort || 0,
level: item.level || 1,
parentId: item.parentId || "",
type: categoryType,
deletable: categoryType === "user",
});
const normalizeFiles = (files) => {
if (!Array.isArray(files)) return [];
return files
.map((item) => {
if (typeof item === "string") return { type: "image", url: normalizeFileUrl(item) };
const url = item?.url || item?.URL || item?.download_url;
if (!url) return null;
return {
type: item.type || "image",
url: normalizeFileUrl(url),
name: item.name || item.fileName || "",
};
})
.filter(Boolean);
};
const normalizePhrase = (item, phraseType) => ({
id: item._id || item.id,
cateId: item.cateId || item.categoryId,
content: item.content || "",
createTime: item.createTime,
updateTime: item.updateTime,
files: normalizeFiles(item.files),
sourceType: item.sourceType,
sourceCommonWordId: item.sourceCommonWordId,
sourceCateId: item.sourceCateId,
collectTime: item.collectTime,
collectClient: item.collectClient,
type: phraseType,
});
const getAccountParams = () => {
const corpId = doctorInfo.value?.corpId;
const userId = doctorInfo.value?.userid;
return { corpId, userId };
};
const isFavorite = (phrase) => Boolean(favoriteMap.value[phrase.id]);
const switchTab = (tab) => {
if (activeTab.value === tab) return;
activeTab.value = tab;
isEditMode.value = false;
searchKeyword.value = "";
currentCategory.value =
tab === "mine"
? firstMyCategory.value?.id || ""
: corpCategories.value[0]?.id || "";
};
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value;
};
const handleCategoryClick = (category) => {
if (activeTab.value === "mine" && isEditMode.value && category.deletable) {
editCategory(category);
return;
}
currentCategory.value = category.id;
};
const sendPhrase = (phrase) => {
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
if (prevPage) {
prevPage.$vm.sendCommonPhrase(phrase);
}
uni.navigateBack();
};
const showAddPhraseDialog = () => {
editingPhrase.value = null;
phraseForm.value = {
content: "",
cateId: currentCategory.value || firstMyCategory.value?.id || "",
files: [],
};
showPhrasePopup.value = true;
};
const editPhrase = (phrase) => {
editingPhrase.value = phrase;
phraseForm.value = {
content: phrase.content,
cateId: phrase.cateId || firstMyCategory.value?.id || "",
files: normalizeFiles(phrase.files),
};
showPhrasePopup.value = true;
};
const handlePhraseCategoryChange = (event) => {
const index = Number(event.detail.value || 0);
phraseForm.value.cateId = myCategories.value[index]?.id || "";
};
const previewFiles = (files, index = 0) => {
const urls = normalizeFiles(files).map((item) => item.url).filter(Boolean);
if (!urls.length) return;
uni.previewImage({ urls, current: urls[index] || urls[0] });
};
const handlePhraseFilesChange = ({ value }) => {
phraseForm.value.files = normalizeFiles(value);
};
const savePhrase = async () => {
if (!phraseForm.value.cateId) {
uni.showToast({ title: "请选择目录", icon: "none" });
return;
}
if (!phraseForm.value.content.trim()) {
uni.showToast({ title: "请输入内容", icon: "none" });
return;
}
const { corpId, userId } = getAccountParams();
if (!corpId || !userId) {
uni.showToast({ title: "请先登录", icon: "none" });
return;
}
try {
const result = await api("setCommonWords", {
id: editingPhrase.value?.id,
cateId: phraseForm.value.cateId,
content: phraseForm.value.content,
files: normalizeFiles(phraseForm.value.files),
corpId,
userId,
});
if (result.success) {
const nextPhrase = normalizePhrase({
...editingPhrase.value,
...(result.data || {}),
_id: editingPhrase.value?.id || result.data?._id,
cateId: phraseForm.value.cateId,
content: phraseForm.value.content.trim(),
files: normalizeFiles(phraseForm.value.files),
}, "user");
if (editingPhrase.value) {
const index = myPhrases.value.findIndex((item) => item.id === editingPhrase.value.id);
if (index > -1) myPhrases.value.splice(index, 1, nextPhrase);
} else if (nextPhrase.id) {
myPhrases.value.unshift(nextPhrase);
} else {
await loadMyPhrases();
}
uni.showToast({ title: "保存成功", icon: "success" });
closePopup();
} else {
uni.showToast({ title: result.message || "操作失败", icon: "none" });
}
} catch (error) {
console.error("保存常用语失败:", error);
uni.showToast({ title: "操作失败", icon: "none" });
}
};
const removeMyPhrase = async (phrase) => {
const { corpId, userId } = getAccountParams();
const result = await api("removeCommonWords", {
id: phrase.id,
corpId,
userId,
});
if (result.success) {
const index = myPhrases.value.findIndex((item) => item.id === phrase.id);
if (index > -1) myPhrases.value.splice(index, 1);
}
return result;
};
const deletePhrase = (phrase) => {
uni.showModal({
title: "提示",
content: "确定删除该常用语吗?",
success: async (res) => {
if (!res.confirm) return;
try {
const result = await removeMyPhrase(phrase);
if (result.success) {
uni.showToast({ title: "删除成功", icon: "success" });
} else {
uni.showToast({ title: result.message || "删除失败", icon: "none" });
}
} catch (error) {
console.error("删除常用语失败:", error);
uni.showToast({ title: "删除失败", icon: "none" });
}
},
});
};
const toggleFavorite = async (phrase) => {
const { corpId, userId } = getAccountParams();
if (!corpId || !userId) {
uni.showToast({ title: "请先登录", icon: "none" });
return;
}
try {
const favorite = favoriteMap.value[phrase.id];
if (favorite) {
const result = await removeMyPhrase(favorite);
if (result.success) {
uni.showToast({ title: "已取消收藏", icon: "success" });
} else {
uni.showToast({ title: result.message || "取消收藏失败", icon: "none" });
}
return;
}
if (myCategories.value.length === 0) {
uni.showToast({ title: "请先新增目录", icon: "none" });
return;
}
collectingPhrase.value = phrase;
collectForm.value.cateId = firstMyCategory.value?.id || "";
showCollectPopup.value = true;
} catch (error) {
console.error("收藏操作失败:", error);
uni.showToast({ title: "操作失败", icon: "none" });
}
};
const handleCollectCategoryChange = (event) => {
const index = Number(event.detail.value || 0);
collectForm.value.cateId = myCategories.value[index]?.id || "";
};
const confirmFavorite = async () => {
const phrase = collectingPhrase.value;
if (!phrase) {
closeCollectPopup();
return;
}
if (!collectForm.value.cateId) {
uni.showToast({ title: "请选择目录", icon: "none" });
return;
}
const { corpId, userId } = getAccountParams();
if (!corpId || !userId) {
uni.showToast({ title: "请先登录", icon: "none" });
return;
}
try {
const result = await api("setCommonWords", {
cateId: collectForm.value.cateId,
content: phrase.content,
corpId,
userId,
sourceType: "common-words",
sourceCommonWordId: phrase.id,
sourceCateId: phrase.cateId,
collectClient: "wxapp",
files: normalizeFiles(phrase.files),
});
if (result.success && result.data) {
myPhrases.value.unshift(normalizePhrase(result.data, "user"));
uni.showToast({ title: "收藏成功", icon: "success" });
closeCollectPopup();
} else if (result.success) {
await loadMyPhrases();
uni.showToast({ title: "收藏成功", icon: "success" });
closeCollectPopup();
} 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 editCategory = (category) => {
editingCategory.value = category;
categoryForm.value.name = category.name;
showCategoryPopup.value = true;
};
const saveCategory = async () => {
if (!categoryForm.value.name.trim()) {
uni.showToast({ title: "请输入分类名", icon: "none" });
return;
}
if (categoryForm.value.name.length > 10) {
uni.showToast({ title: "输入内容超过10个字", icon: "none" });
return;
}
try {
const { corpId, userId } = getAccountParams();
const result = await api(editingCategory.value ? "updateUserCommonWordCate" : "addUserCommonWordCate", {
id: editingCategory.value?.id,
label: categoryForm.value.name,
corpId,
userId,
});
if (result.success) {
await loadMyCategories();
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 (activeTab.value !== "mine" || !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, userId } = getAccountParams();
const result = await api("deleteUserCommonWordCate", {
id: category.id,
corpId,
userId,
});
if (result.success) {
await loadMyCategories();
await loadMyPhrases();
currentCategory.value = firstMyCategory.value?.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;
editingPhrase.value = null;
};
const closeCollectPopup = () => {
showCollectPopup.value = false;
collectingPhrase.value = null;
collectForm.value.cateId = "";
};
const closeCategoryPopup = () => {
showCategoryPopup.value = false;
editingCategory.value = null;
};
const loadMyCategories = async () => {
const { corpId, userId } = getAccountParams();
const result = await api("getUserCommonWordCate", { corpId, userId });
if (result.success) {
const list = Array.isArray(result.list) ? result.list : [];
myCategories.value = list.map((item) => normalizeCategory(item, "user")).filter((item) => item.id);
if (activeTab.value === "mine") {
currentCategory.value = currentCategory.value || firstMyCategory.value?.id || "";
}
} else {
uni.showToast({ title: result.message || "加载失败", icon: "none" });
}
};
const loadMyPhrases = async () => {
const { corpId, userId } = getAccountParams();
const cateIds = myCategories.value.map((item) => item.id).filter(Boolean);
if (cateIds.length === 0) {
myPhrases.value = [];
return;
}
const result = await api("getCommonWordsList", {
corpId,
userId,
cateIds,
page: 1,
pageSize: 10000,
}, false);
if (result.success) {
const list = Array.isArray(result.list) ? result.list : [];
myPhrases.value = list.map((item) => normalizePhrase(item, "user")).filter((item) => item.id);
}
};
const loadCorpCategories = async () => {
const { corpId, userId } = getAccountParams();
const result = await api("getCorpCommonWordCate", { corpId, userId }, false);
if (result.success) {
const list = Array.isArray(result.list) ? result.list : [];
corpCategories.value = list.map((item) => normalizeCategory(item, "corp")).filter((item) => item.id);
if (activeTab.value === "more" && !currentCategory.value) {
currentCategory.value = corpCategories.value[0]?.id || "";
}
}
};
const loadCorpPhrases = async () => {
const { corpId, userId } = getAccountParams();
const cateIds = corpCategories.value.map((item) => item.id).filter(Boolean);
if (cateIds.length === 0) {
corpPhrases.value = [];
return;
}
const result = await api("getCommonWordsList", {
corpId,
userId,
cateIds,
page: 1,
pageSize: 10000,
}, false);
if (result.success) {
const list = Array.isArray(result.list) ? result.list : [];
corpPhrases.value = list.map((item) => normalizePhrase(item, "corp")).filter((item) => item.id);
}
};
const loadAll = async () => {
const { corpId, userId } = getAccountParams();
if (!corpId || !userId) {
uni.showToast({ title: "请先登录", icon: "none" });
return;
}
try {
await loadMyCategories();
await loadMyPhrases();
await loadCorpCategories();
await loadCorpPhrases();
} catch (error) {
console.error("加载常用语失败:", error);
uni.showToast({ title: "加载失败", icon: "none" });
}
};
onMounted(() => {
loadAll();
});
</script>
<style scoped lang="scss">
$primary-color: #1f5cff;
$primary-light: #eef4ff;
$text-main: #222;
$border-color: #edf0f5;
.common-phrases-page {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 20rpx;
display: flex;
flex-direction: column;
background: #f7f8fb;
}
.tabs-bar {
display: flex;
height: 96rpx;
background: #fff;
border-bottom: 1px solid $border-color;
.tab-item {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
color: #4b5565;
font-size: 30rpx;
font-weight: 500;
&.active {
color: $primary-color;
font-weight: 700;
&::after {
content: "";
position: absolute;
left: 50%;
bottom: 14rpx;
width: 70rpx;
height: 6rpx;
border-radius: 999rpx;
background: $primary-color;
transform: translateX(-50%);
}
}
}
}
.content-wrapper {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
.category-sidebar {
width: 210rpx;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: #fff;
border-right: 1px solid $border-color;
.category-list {
flex: 1;
min-height: 0;
}
.category-item {
position: relative;
min-height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 12rpx 16rpx;
color: #344054;
background: #fff;
&.active {
color: $primary-color;
background: $primary-light;
&::before {
content: "";
position: absolute;
left: 0;
top: 20rpx;
bottom: 20rpx;
width: 4rpx;
border-radius: 999rpx;
background: $primary-color;
}
.category-name {
font-weight: 700;
}
}
}
.category-content {
position: relative;
width: 100%;
text-align: center;
}
.category-name {
font-size: 28rpx;
line-height: 40rpx;
word-break: break-all;
}
.delete-badge {
position: absolute;
top: -22rpx;
right: -8rpx;
width: 30rpx;
height: 30rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #ff3b30;
.delete-icon {
color: #fff;
font-size: 24rpx;
line-height: 1;
}
}
.add-category-footer {
min-height: 104rpx;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid $border-color;
background: #fff;
.add-category-text {
color: $primary-color;
font-size: 28rpx;
font-weight: 600;
}
}
}
.phrases-container {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
background: #f7f8fb;
}
.list-header {
padding: 22rpx 24rpx 14rpx;
.current-title {
margin-bottom: 16rpx;
color: $text-main;
font-size: 28rpx;
font-weight: 700;
}
}
.search-box {
height: 64rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
border-radius: 32rpx;
background: #fff;
border: 1px solid $border-color;
.search-icon {
margin-right: 10rpx;
color: #98a2b3;
font-size: 30rpx;
}
.search-input {
flex: 1;
min-width: 0;
height: 64rpx;
color: $text-main;
font-size: 26rpx;
}
.clear-search {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
color: #98a2b3;
font-size: 34rpx;
}
}
.phrases-list {
flex: 1;
min-height: 0;
padding: 0 24rpx 24rpx;
box-sizing: border-box;
}
.phrase-item {
margin-bottom: 20rpx;
padding: 24rpx 24rpx 18rpx;
border-radius: 14rpx;
background: #fff;
border: 1px solid $border-color;
box-shadow: 0 8rpx 24rpx rgba(16, 24, 40, 0.04);
.phrase-top {
display: flex;
align-items: flex-start;
gap: 12rpx;
}
.phrase-content {
flex: 1;
min-width: 0;
color: $text-main;
font-size: 28rpx;
line-height: 44rpx;
word-break: break-word;
white-space: pre-wrap;
}
.collected-badge {
flex-shrink: 0;
padding: 4rpx 10rpx;
color: $primary-color;
font-size: 22rpx;
border-radius: 6rpx;
background: $primary-light;
}
.phrase-actions {
display: flex;
align-items: center;
gap: 28rpx;
margin-top: 20rpx;
padding-top: 16rpx;
border-top: 1px solid $border-color;
}
.action-btn {
display: flex;
align-items: center;
gap: 6rpx;
color: #475467;
font-size: 26rpx;
.action-icon {
font-size: 30rpx;
line-height: 1;
}
&.send,
&.collect.collected {
color: $primary-color;
}
&.delete {
color: #ff3b30;
}
}
}
.phrase-images {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.phrase-image {
width: 96rpx;
height: 96rpx;
border-radius: 10rpx;
background: #f2f4f7;
}
.collect-tip {
padding: 8rpx 0 28rpx;
color: #98a2b3;
font-size: 24rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 180rpx 24rpx;
text-align: center;
.empty-text {
color: #667085;
font-size: 30rpx;
font-weight: 600;
}
.empty-hint {
margin-top: 14rpx;
color: #98a2b3;
font-size: 24rpx;
}
}
.footer-action-bar {
display: flex;
align-items: center;
gap: 18rpx;
padding: 22rpx 24rpx 28rpx;
background: #fff;
border-top: 1px solid $border-color;
.add-phrase-btn {
flex: 1;
height: 76rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
color: #fff;
background: $primary-color;
border-radius: 18rpx;
box-shadow: 0 8rpx 20rpx rgba(31, 92, 255, 0.22);
}
.add-icon,
.add-text,
.edit-text {
font-size: 28rpx;
font-weight: 600;
}
.edit-btn {
width: 112rpx;
height: 76rpx;
display: flex;
align-items: center;
justify-content: center;
color: $primary-color;
}
}
.popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: flex-end;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
}
.popup-content {
width: 100%;
padding: 34rpx 32rpx 40rpx;
box-sizing: border-box;
background: #fff;
border-radius: 28rpx 28rpx 0 0;
box-shadow: 0 -10rpx 30rpx rgba(16, 24, 40, 0.12);
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.popup-title {
color: $text-main;
font-size: 34rpx;
font-weight: 700;
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
.close-icon {
color: #98a2b3;
font-size: 46rpx;
line-height: 1;
}
}
.form-label {
margin-bottom: 14rpx;
color: $text-main;
font-size: 26rpx;
font-weight: 600;
}
.content-label {
margin-top: 30rpx;
}
.category-picker,
.category-input {
height: 84rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22rpx;
box-sizing: border-box;
color: $text-main;
font-size: 28rpx;
border: 1px solid #d9e0ea;
border-radius: 12rpx;
background: #fff;
}
.picker-arrow {
color: #667085;
font-size: 32rpx;
}
.form-hint {
margin-top: 12rpx;
color: #98a2b3;
font-size: 24rpx;
}
.collect-preview {
margin: 28rpx 0 32rpx;
padding: 22rpx;
color: $text-main;
font-size: 28rpx;
line-height: 42rpx;
word-break: break-word;
white-space: pre-wrap;
border-radius: 12rpx;
background: #f7f8fb;
border: 1px solid $border-color;
}
.collect-images {
margin: -14rpx 0 32rpx;
}
.phrase-files-cell {
display: block;
margin-bottom: 30rpx;
:deep(.files-wrap) {
padding: 0;
border-bottom: 0;
background: transparent;
}
:deep(.files-label) {
color: $text-main;
font-size: 28rpx;
font-weight: 500;
}
}
.phrase-textarea {
width: 100%;
height: 220rpx;
padding: 22rpx;
box-sizing: border-box;
color: $text-main;
font-size: 28rpx;
line-height: 42rpx;
border: 1px solid #d9e0ea;
border-radius: 12rpx;
background: #fff;
}
.char-count {
margin-top: 12rpx;
margin-bottom: 30rpx;
color: #98a2b3;
font-size: 24rpx;
text-align: right;
}
.category-popup {
.category-input {
width: 100%;
margin-bottom: 32rpx;
}
}
.popup-actions {
display: flex;
gap: 24rpx;
button {
flex: 1;
height: 78rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
font-size: 28rpx;
font-weight: 600;
border-radius: 14rpx;
&::after {
border: 0;
}
}
.cancel-btn {
color: $text-main;
background: #fff;
border: 1px solid #d9e0ea;
}
.confirm-btn {
color: #fff;
background: $primary-color;
}
}
</style>