ykt-wxapp/pages/message/article-list.vue
2026-05-28 16:37:57 +08:00

739 lines
20 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>
<full-page :customScroll="true" pageStyle="background:#f5f5f5">
<template #header>
<view class="header">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999" />
<input class="search-input" v-model="searchTitle" placeholder="输入内容名称搜索" @input="handleSearch" />
</view>
</view>
</template>
<view v-if="showSearch" class="h-full bg-white">
<scroll-view scroll-y class="h-full" @scrolltolower="getMore" lower-threshold="50">
<view v-if="loading && searchList.length === 0" class="loading-container">
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="searchList.length === 0" class="empty-container">
<empty-data title="暂无文章" />
</view>
<view v-else>
<view v-for="article in searchList" :key="article._id" class="article-item" @click="previewArticle(article)">
<view>
<view class="flex items-center py-12 px-15">
<view class="text-lg leading-normal font-semibold w-0 flex-grow truncate mr-10">
{{ article.title }}
</view>
<view v-if="!article.isMine" @click.stop="star(article)">
<uni-icons class="flex-shrinl-0" :type="article.star ? 'star-filled' : 'star'"
:color="article.star ? '#FFD700' : '#999'" size="20"></uni-icons>
</view>
</view>
<view class="flex items-center px-15 pb-10">
<view class="mr-10 w-0 flex-growt runcate text-base text-dark">{{ article.authorName }}</view>
<view class="bg-primary text-white text-sm px-10 leading-normal rounded-sm"
@click.stop="handlePrimaryAction(article)">
{{ isSelectMode ? '选择' : '发送' }}
</view>
</view>
</view>
</view>
<view v-if="loading && searchList.length > 0" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999" />
<text>加载中...</text>
</view>
<view v-if="!loading && searchList.length >= total" class="no-more">
没有更多了
</view>
<view v-if="searchHasMore" class="no-more" @click="getSearchMore()">
加载更多
</view>
</view>
</scroll-view>
</view>
<view v-else class="h-full flex">
<scroll-view scroll-y class="flex-shrink-0 category-sidebar h-full">
<view v-for="cate in cateList" :key="cate.value" class="category-item"
:class="{ active: currentCateId === cate.value }" @click="selectCategory(cate)">
{{ cate.label }}
</view>
</scroll-view>
<scroll-view scroll-y class="w-0 flex-grow bg-white" @scrolltolower="getMore" lower-threshold="50">
<view v-if="loading && articleList.length === 0" class="loading-container">
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="articleList.length === 0" class="empty-container">
<empty-data title="暂无文章" />
</view>
<view v-else>
<view v-for="article in articleList" :key="article._id" class="article-item" @click="previewArticle(article)">
<view>
<view class="flex items-center py-12 px-15">
<view class="text-lg leading-normal font-semibold w-0 flex-grow truncate mr-10">
{{ article.title }}
</view>
<view v-if="!article.isMine" @click.stop="star(article)">
<uni-icons class="flex-shrinl-0" :type="article.star ? 'star-filled' : 'star'"
:color="article.star ? '#FFD700' : '#999'" size="20"></uni-icons>
</view>
</view>
<view class="flex items-center px-15 pb-10">
<view class="mr-10 w-0 flex-grow truncate text-base text-dark">{{ article.authorName }}</view>
<view class="bg-primary text-white text-sm px-10 leading-normal rounded-sm"
@click.stop="handlePrimaryAction(article)">
{{ isSelectMode ? '选择' : '发送' }}
</view>
</view>
<!-- <view class="border-b"></view>
<view class="flex items-center justify-between py-10 px-15">
<view class="flex items-center" @click="previewArticle(article)">
<view class="text-base text-primary">文章详情</view>
<uni-icons type="right" color="#0074ff" size="16"></uni-icons>
</view>
<view class="bg-primary text-white text-base px-15 py-5 rounded-sm"
@click.stop="handlePrimaryAction(article)">
{{ isSelectMode ? '选择' : '发送' }}
</view>
</view> -->
<!-- <view class="article-footer">
<text class="article-date">{{ article.date }}</text>
<button class="send-btn" size="mini" type="primary" @click.stop="handlePrimaryAction(article)">
{{ isSelectMode ? '选择' : '发送' }}
</button>
</view> -->
</view>
</view>
<view v-if="loading && articleList.length > 0" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999" />
<text>加载中...</text>
</view>
<view v-if="!loading && articleList.length >= total" class="no-more">
没有更多了
</view>
<view v-if="hasMore" class="no-more" @click="getMore()">
加载更多
</view>
</view>
</scroll-view>
</view>
<template #footer>
<view class="relative shadow-up bg-white">
<view class="p-15 text-center" @click="toService()">
<text class="mr-5 text-base text-gray">如没有符合的内容</text>
<text class="text-base text-primary">联系客服</text>
</view>
</view>
</template>
</full-page>
<!-- 文章预览弹窗 -->
<uni-popup ref="previewPopup" type="bottom" :safe-area="false">
<view class="preview-container">
<view class="preview-header">
<text class="preview-title">{{ previewArticleData.title }}</text>
<view class="preview-close" @click="closePreview">
<uni-icons type="closeempty" size="24" color="#333" />
</view>
</view>
<scroll-view scroll-y class="preview-content">
<view class="rich-text-wrapper">
<rich-text :nodes="previewArticleData.content"></rich-text>
</view>
</scroll-view>
<view class="preview-footer">
<button class="preview-close-btn" @click="closePreview">关闭</button>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, computed, watch } from "vue";
import { storeToRefs } from "pinia";
import { onLoad, onUnload, onShow } from "@dcloudio/uni-app";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import useGuard from "@/hooks/useGuard.js";
import usePageList from "@/hooks/usePageList";
import { sendArticleMessage } from "@/utils/send-message-helper.js";
import fullPage from '@/components/full-page.vue';
import EmptyData from "@/components/empty-data.vue";
import { toast, loading as showLoading, hideLoading } from "@/utils/widget";
const accountStore = useAccountStore();
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
const { doctorInfo, account } = storeToRefs(accountStore);
const { useLoad } = useGuard()
const { total, page, pageSize, list: articleList, pages, loading, hasMore } = usePageList();
const { total: searchTotal, page: searchPage, pageSize: searchPageSize, list: searchList, pages: searchPages, loading: searchLoading, hasMore: searchHasMore } = usePageList();
const searchTitle = ref("");
let searchTimer = null;
// 从页面参数获取群组信息
const pageParams = ref({
groupId: "",
patientId: "",
corpId: "",
teamId: "",
});
const currentCateId = ref("my"); // 默认选中"全部"_id 为空字符串)
const teams = ref([])
const showRefresh = ref(false);
const cateList = computed(() => {
const arr = [{ label: '我的文章', value: 'my', type: 'mine' }, { label: '团队文章', value: 'team', type: 'team' }];
if (doctorInfo.value && doctorInfo.value.hospitalId) {
arr.push({ label: doctorInfo.value.hospitalName, value: doctorInfo.value.hospitalId, type: 'firstHospital' });
}
if (doctorInfo.value && doctorInfo.value.secondPracticeHospitalId) {
arr.push({ label: doctorInfo.value.secondPracticeHospitalName, value: doctorInfo.value.secondPracticeHospitalId, type: 'secondHospital' });
}
arr.push({ label: '柚助手', value: 'corp', type: 'yzs' })
return arr;
})
const userId = computed(() => doctorInfo.value?.userid || "");
const showSearch = computed(() => searchTitle.value.trim() !== '')
const ensureTeamId = async () => {
if (pageParams.value.teamId) return pageParams.value.teamId;
if (!pageParams.value.groupId) return "";
try {
const res = await api("getGroupListByGroupId", { groupId: pageParams.value.groupId }, false);
const team = res?.data?.team || {};
const resolved = res?.data?.teamId || team.teamId || team._id || "";
if (resolved) pageParams.value.teamId = resolved;
return pageParams.value.teamId;
} catch (err) {
console.error("ensureTeamId failed:", err);
return "";
}
};
const isSelectMode = ref(false);
const selectEventName = ref("");
// 搜索关键词
// 预览文章数据
const previewArticleData = ref({
title: "",
content: "",
});
const previewPopup = ref(null);
// 选择分类
const selectCategory = (cate) => {
currentCateId.value = cate.value || "";
page.value = 1;
getArticleList();
};
// 搜索处理
const handleSearch = () => {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(() => {
searchPage.value = 1;
searchArticleList();
}, 1000);
};
// 处理富文本内容,使图片自适应
const processRichTextContent = (html) => {
if (!html) return "";
// 给所有 img 标签添加样式
let processedHtml = html.replace(
/<img/gi,
'<img style="max-width:100%;height:auto;display:block;margin:10px 0;"'
);
// 移除可能存在的固定宽度样式
processedHtml = processedHtml.replace(
/style="[^"]*width:\s*\d+px[^"]*"/gi,
(match) => {
return match.replace(/width:\s*\d+px;?/gi, "max-width:100%;");
}
);
// 处理表格,添加自适应样式
processedHtml = processedHtml.replace(
/<table/gi,
'<table style="max-width:100%;overflow-x:auto;display:block;"'
);
// 给整体内容添加容器样式
processedHtml = `<div style="width:100%;overflow-x:hidden;word-wrap:break-word;word-break:break-all;">${processedHtml}</div>`;
return processedHtml;
};
// 预览文章
const previewArticle = async (article) => {
uni.navigateTo({
url: `/pages/message/article-detail?id=${article._id}`,
});
};
// 关闭预览
const closePreview = () => {
previewPopup.value?.close();
};
const selectArticle = (article) => {
if (!selectEventName.value) {
uni.showToast({ title: "缺少 eventName", icon: "none" });
return;
}
uni.$emit(selectEventName.value, article);
uni.navigateBack();
};
const handlePrimaryAction = (article) => {
if (isSelectMode.value) return selectArticle(article);
return sendArticle(article);
};
// 发送文章
const sendArticle = async (article) => {
try {
const { doctorInfo } = useAccountStore();
await ensureTeamId();
// 使用统一的消息发送助手
const success = await sendArticleMessage(
{
_id: article._id,
title: article.title || "宣教文章",
cover: article.cover || "",
url: article.url || "",
subtitle: article.subtitle || "",
wechatChannels: article.wechatChannels,
},
{
articleId: article._id,
userId: doctorInfo?.userid,
customerId: pageParams.value.patientId,
corpId: corpId,
teamId: pageParams.value.teamId,
}
);
if (success) {
uni.navigateBack();
}
} catch (error) {
console.error("发送文章失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
} finally {
loading.value = false;
}
};
function toService() {
uni.navigateTo({
url: '/pages/work/service/contact-service'
})
}
async function getArticleList() {
if (!userId.value) {
articleList.value = []
return
}
if (currentCateId.value === 'my') {
getMyArticleList();
} else if (currentCateId.value === 'team') {
getTeamArticleList();
} else if (currentCateId.value === 'corp') {
getSourceCorpArticleList();
} else {
getOrganizationArticleList();
}
}
async function getTeams() {
const corpId = account.value?.corpId;
const mateId = doctorInfo.value?.userid;
if (!corpId || !mateId) return;
const res = await api('getJoinedTeams', { corpId, mateId });
teams.value = res && Array.isArray(res.data) ? res.data : [];
}
async function getMyArticleList() {
showLoading();
const res = await api("getMyArticles", { userId: userId.value });
hideLoading();
articleList.value = res && Array.isArray(res.list) ? formatArticles(res.list) : [];
pages.value = 1;
}
async function getTeamArticleList() {
showLoading();
if (teams.value.length === 0) {
await getTeams();
}
const teamIds = teams.value.map(i => i.teamId);
const res = await api("getMyTeamArticles", { userId: userId.value, teamIds, page: page.value, pageSize: pageSize.value });
hideLoading();
articleList.value = res && Array.isArray(res.list) ? formatArticles(res.list) : [];
pages.value = typeof res.pages === 'number' ? res.pages : 1;
}
async function getOrganizationArticleList() {
showLoading();
const query = {
userId: userId.value,
hospitalId: currentCateId.value,
page: page.value,
pageSize: pageSize.value,
}
const res = await api("getOrganizationArticles", query);
hideLoading();
articleList.value = res && Array.isArray(res.list) ? formatArticles(res.list) : [];
pages.value = typeof res.pages === 'number' ? res.pages : 1;
}
async function getSourceCorpArticleList() {
showLoading();
const query = {
userId: userId.value,
sourceCorpId: corpId,
page: page.value,
pageSize: pageSize.value,
}
const res = await api("getSourceCorpArticles", query);
hideLoading();
articleList.value = res && Array.isArray(res.list) ? formatArticles(res.list) : [];
pages.value = typeof res.pages === 'number' ? res.pages : 1;
}
async function searchArticleList() {
if (searchTitle.value.trim() === '') {
searchList.value = [];
searchPages.value = 1;
return
}
showLoading();
const query = {
userId: userId.value,
page: searchPage.value,
pageSize: searchPageSize.value,
keyword: searchTitle.value.trim(),
}
const res = await api("searchCorpArticles", query);
hideLoading();
searchList.value = res && Array.isArray(res.list) ? formatArticles(res.list) : [];
searchPages.value = typeof res.pages === 'number' ? res.pages : 1;
}
async function star(article) {
showLoading();
const res = await api("starArticle", { articleId: article._id, userId: userId.value, star: !article.star });
hideLoading();
if (res.success) {
article.star = !article.star;
if (currentCateId.value === 'my') {
getMyArticleList();
}
} else {
toast(res.message || "操作失败");
}
}
function formatArticles(articles) {
return articles.map(article => {
const item = { ...article };
if (article.creatorSignature && article.creatorSignature.type === 'personal') {
item.isMine = article.creatorSignature.person.userId === userId.value;
item.authorName = article.creatorSignature.person ? article.creatorSignature.person.userName : '';
} else if (article.creatorSignature && article.creatorSignature.type === 'institution') {
item.authorName = article.creatorSignature.institution ? article.creatorSignature.institution.name : '';
} else if (article.creatorSignature && article.creatorSignature.type === 'department') {
item.authorName = article.creatorSignature.department ? article.creatorSignature.department.name : '';
}
return item;
});
}
function getMore() {
if (hasMore.value && !loading.value) {
page.value++;
getArticleList();
}
}
function getSearchMore() {
if (searchHasMore.value && !loading.value) {
searchPage.value++;
searchArticleList();
}
}
// 页面加载时接收参数
onLoad((options) => {
isSelectMode.value = String(options?.select || '') === '1';
selectEventName.value = String(options?.eventName || '');
if (options.groupId) {
pageParams.value.groupId = options.groupId;
}
if (options.patientId) {
pageParams.value.patientId = options.patientId;
}
if (options.corpId) {
pageParams.value.corpId = options.corpId;
}
if (options.teamId) {
pageParams.value.teamId = options.teamId;
}
ensureTeamId();
uni.$on('changeArticleStar', (articleId) => {
if (articleList.value.some(i => i._id === articleId)) {
showRefresh.value = true;
}
});
});
useLoad(() => { });
onUnload(() => {
uni.$off('changeArticleStar');
});
onShow(() => {
if (showRefresh.value) {
page.value = 1;
getArticleList();
showRefresh.value = false;
}
});
watch(userId, n => {
if (n) {
getArticleList();
}
}, { immediate: true })
</script>
<style scoped lang="scss">
.article-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.header {
background-color: #fff;
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.search-bar {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 16rpx 24rpx;
}
.search-input {
flex: 1;
margin-left: 16rpx;
font-size: 30rpx;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.category-sidebar {
width: 200rpx;
background-color: #f8f8f8;
border-right: 1px solid #eee;
}
.category-item {
padding: 20rpx 24rpx;
font-size: 30rpx;
color: #333;
text-align: center;
border-bottom: 1px solid #eee;
transition: all 0.3s ease;
position: relative;
}
.category-item.active {
background-color: #fff;
color: #0877f1;
font-weight: bold;
border-left: 4rpx solid #0877f1;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text {
margin-top: 20rpx;
font-size: 30rpx;
color: #999;
}
.article-item {
border-bottom: 1px solid #eee;
transition: background-color 0.2s ease;
}
.article-item:active {
background-color: #f5f5f5;
}
.article-content {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.article-title {
font-size: 30rpx;
color: #333;
line-height: 1.6;
word-break: break-all;
font-weight: 500;
}
.article-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.article-date {
flex: 1;
font-size: 26rpx;
color: #999;
}
.send-btn {
flex-shrink: 0;
font-size: 28rpx;
padding: 8rpx 32rpx;
height: auto;
line-height: 1.4;
}
.loading-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 26rpx;
color: #999;
gap: 10rpx;
}
.footer {
background-color: #fff;
padding: 20rpx;
border-top: 1px solid #eee;
}
.cancel-btn {
width: 100%;
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
/* 预览弹窗样式 */
.preview-container {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
height: 80vh;
display: flex;
flex-direction: column;
}
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1px solid #eee;
}
.preview-title {
flex: 1;
font-size: 34rpx;
font-weight: bold;
color: #333;
}
.preview-close {
padding: 10rpx;
}
.preview-content {
flex: 1;
padding: 0;
overflow-y: auto;
}
.rich-text-wrapper {
padding: 30rpx;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
/* rich-text 内部样式 */
.rich-text-wrapper ::v-deep rich-text {
width: 100%;
}
.rich-text-wrapper ::v-deep rich-text img {
max-width: 100% !important;
height: auto !important;
display: block;
}
.preview-footer {
padding: 20rpx;
border-top: 1px solid #eee;
}
.preview-close-btn {
width: 100%;
background-color: #1890ff;
color: #fff;
}
</style>