ykt-wxapp/pages/message/article-list.vue
2026-01-28 16:35:14 +08:00

584 lines
13 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="article-page">
<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>
<view class="content">
<view class="category-sidebar">
<scroll-view scroll-y class="category-scroll">
<view
v-for="cate in categoryList"
:key="cate._id || 'all'"
class="category-item"
:class="{ active: currentCateId === cate._id }"
@click="selectCategory(cate)"
>
{{ cate.label }}
</view>
</scroll-view>
</view>
<view class="article-list">
<scroll-view
scroll-y
class="article-scroll"
@scrolltolower="loadMore"
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"
>
<view class="article-content" @click="previewArticle(article)">
<text class="article-title">{{ article.title }}</text>
<view class="article-footer">
<text class="article-date">{{ article.date }}</text>
<button
class="send-btn"
size="mini"
type="primary"
@click.stop="sendArticle(article)"
>
发送
</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>
</scroll-view>
</view>
</view>
<!-- 文章预览弹窗 -->
<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>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
const accountStore = useAccountStore();
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
// 从页面参数获取群组信息
const pageParams = ref({
groupId: "",
userId: "",
corpId: "",
});
// 搜索关键词
const searchTitle = ref("");
let searchTimer = null;
// 分类列表
const categoryList = ref([{ _id: "", label: "全部" }]);
const currentCateId = ref(""); // 默认选中"全部"_id 为空字符串)
// 文章列表
const articleList = ref([]);
const loading = ref(false);
const page = ref(1);
const pageSize = 30;
const total = ref(0);
// 预览文章数据
const previewArticleData = ref({
title: "",
content: "",
});
const previewPopup = ref(null);
// 获取分类列表
const getCategoryList = async () => {
try {
const res = await api("getArticleCateList", { corpId: corpId });
if (res.success && res.list) {
const cates = res.list || [];
categoryList.value = [{ _id: "", label: "全部" }, ...cates];
}
} catch (error) {
console.error("获取分类列表失败:", error);
}
};
// 选择分类
const selectCategory = (cate) => {
currentCateId.value = cate._id || "";
page.value = 1;
articleList.value = [];
loadArticleList();
};
// 搜索处理
const handleSearch = () => {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(() => {
page.value = 1;
articleList.value = [];
loadArticleList();
}, 500);
};
// 加载文章列表
const loadArticleList = async () => {
if (loading.value) return;
loading.value = true;
try {
const params = {
corpId: corpId,
page: page.value,
pageSize: pageSize,
enable: true,
title: searchTitle.value,
};
// 如果选择了分类添加分类ID
if (currentCateId.value) {
params.cateIds = [currentCateId.value];
}
const res = await api("getArticleList", params);
if (res.success && res.list) {
const { list = [], total: count = 0 } = res;
const formattedList = list.map((item) => {
// 格式化日期
let date = "";
if (item.createTime) {
const d = new Date(item.createTime);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
date = `${year}-${month}-${day}`;
}
return {
...item,
date,
};
});
if (page.value === 1) {
articleList.value = formattedList;
} else {
articleList.value = [...articleList.value, ...formattedList];
}
total.value = count;
} else {
uni.showToast({
title: res.message || "获取文章列表失败",
icon: "none",
});
}
} catch (error) {
console.error("加载文章列表失败:", error);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
} finally {
loading.value = false;
}
};
// 加载更多
const loadMore = () => {
if (loading.value || articleList.value.length >= total.value) return;
page.value += 1;
loadArticleList();
};
// 处理富文本内容,使图片自适应
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) => {
try {
uni.showLoading({ title: "加载中..." });
const res = await api("getArticle", { id: article._id, corpId: corpId });
uni.hideLoading();
if (res.success && res.data) {
previewArticleData.value = {
title: res.data.title || article.title,
content: processRichTextContent(res.data.content || ""),
};
previewPopup.value?.open();
} else {
uni.showToast({
title: res.message || "预览文章失败",
icon: "none",
});
}
} catch (error) {
uni.hideLoading();
console.error("预览文章失败:", error);
uni.showToast({
title: "预览失败,请重试",
icon: "none",
});
}
};
// 关闭预览
const closePreview = () => {
previewPopup.value?.close();
};
// 发送文章
const sendArticle = async (article) => {
try {
const { doctorInfo } = useAccountStore();
const result = await api("sendArticleMessage", {
groupId: pageParams.value.groupId,
fromAccount: doctorInfo.userid,
articleId: article._id,
title: article.title || "宣教文章",
imgUrl: article.cover || "",
desc: "点击查看详情",
});
if (result.success) {
uni.navigateBack();
} else {
throw new Error(result.message || "发送失败");
}
} catch (error) {
console.error("发送文章失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
} finally {
loading.value = false;
}
};
// 返回
const goBack = () => {
uni.navigateBack();
};
// 页面加载时接收参数
onLoad((options) => {
if (options.groupId) {
pageParams.value.groupId = options.groupId;
}
if (options.userId) {
pageParams.value.userId = options.userId;
}
if (options.corpId) {
pageParams.value.corpId = options.corpId;
}
});
onMounted(() => {
getCategoryList();
loadArticleList();
});
</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: 28rpx;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.category-sidebar {
width: 200rpx;
background-color: #f8f8f8;
border-right: 1px solid #eee;
}
.category-scroll {
height: 100%;
}
.category-item {
padding: 20rpx 24rpx;
font-size: 28rpx;
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;
}
.article-list {
flex: 1;
background-color: #fff;
}
.article-scroll {
height: 100%;
}
.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: 28rpx;
color: #999;
}
.article-item {
padding: 24rpx 30rpx;
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: 28rpx;
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: 24rpx;
color: #999;
}
.send-btn {
flex-shrink: 0;
font-size: 26rpx;
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: 24rpx;
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: 32rpx;
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>