Merge commit 'c1a0090279f17158728df0f16d3860436867b47c' into dev-wdb

# Conflicts:
#	pages/home/customer-archive.vue
This commit is contained in:
wangdongbo 2026-02-05 09:55:20 +08:00
commit 99152578df
5 changed files with 497 additions and 53 deletions

View File

@ -13,6 +13,12 @@
"navigationBarTitleText": "我的宣教"
}
},
{
"path": "pages/article/article-cate-list",
"style": {
"navigationBarTitleText": "健康宣教"
}
},
{
"path": "pages/survey/survey-list",
"style": {
@ -135,12 +141,12 @@
"navigationBarTitleText": "宣教文章"
}
},
{
"path": "pages/article/send-article",
"style": {
"navigationBarTitleText": "选择宣教文章"
}
}
{
"path": "pages/article/send-article",
"style": {
"navigationBarTitleText": "选择宣教文章"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",

View File

@ -0,0 +1,410 @@
<template>
<view class="bg-gray-100 min-h-screen">
<!-- Category Tabs -->
<scroll-view
scroll-x
class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full"
:show-scrollbar="false"
>
<view
v-for="(tab, index) in tabs"
:key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors"
:class="[
activeCateId === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
: 'bg-white text-gray-600 border-gray-200',
]"
@click="selectCate(tab.value)"
>
{{ tab.name }}
</view>
</scroll-view>
<!-- List -->
<view v-if="loading && list.length === 0" class="loading-container">
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="!loading && list.length === 0" class="empty-container">
<empty-data text="暂无文章" />
</view>
<view v-else class="p-15">
<view
v-for="(item, index) in list"
:key="item._id"
class="article-card flex"
:class="{ 'mb-15': index < list.length - 1 }"
@click="goToDetail(item)"
>
<image
class="flex-shrink-0 cover"
:src="item.cover || '/static/home/health-education.png'"
mode="aspectFill"
/>
<view class="w-0 flex-grow">
<view class="article-title truncate mb-10">
{{ item.title || "宣教文章" }}
</view>
<view v-if="item.summary" class="article-summary line-clamp-2">
{{ item.summary }}
</view>
</view>
</view>
<view v-if="loading && list.length > 0" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999" />
<text>加载中...</text>
</view>
<view v-if="!loading && list.length >= total" class="no-more">
没有更多了
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import dayjs from "dayjs";
import api from "@/utils/api.js";
import EmptyData from "@/components/empty-data.vue";
const env = __VITE_ENV__;
const defaultCorpId = env.MP_CORP_ID;
const corpId = ref("");
const tabs = ref([{ name: "全部", value: "" }]);
const activeCateId = ref("");
const list = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = 20;
const loading = ref(false);
const loadCates = async () => {
if (!corpId.value) return;
try {
const res = await api("getArticleCateList", { corpId: corpId.value });
const cates = res && Array.isArray(res.list) ? res.list : [];
tabs.value = [
{ name: "全部", value: "" },
...cates.map((i) => ({ name: i.label || i.name || "未命名", value: i._id })),
];
} catch (err) {
console.error("loadCates failed:", err);
}
};
const loadList = async (reset = false) => {
if (!corpId.value || loading.value) return;
if (reset) {
page.value = 1;
list.value = [];
total.value = 0;
}
loading.value = true;
try {
const params = {
corpId: corpId.value,
page: page.value,
pageSize,
enable: true,
};
if (activeCateId.value) params.cateIds = [activeCateId.value];
const res = await api("getArticleList", params);
const rows = res && Array.isArray(res.list) ? res.list : [];
total.value = Number(res?.total) || 0;
const mapped = rows.map((r) => ({
...r,
time: r?.createTime ? dayjs(r.createTime).format("YYYY-MM-DD") : "",
}));
if (page.value === 1) list.value = mapped;
else list.value = [...list.value, ...mapped];
} catch (err) {
console.error("loadList failed:", err);
} finally {
loading.value = false;
}
};
const selectCate = async (cateId) => {
if (activeCateId.value === cateId) return;
activeCateId.value = cateId;
await loadList(true);
};
function goToDetail(item) {
if (!item?._id) return;
uni.navigateTo({ url: `/pages/article/article-detail?id=${item._id}` });
}
onLoad(async (options) => {
corpId.value = options?.corpId || defaultCorpId || "";
await loadCates();
await loadList(true);
});
onReachBottom(() => {
if (loading.value) return;
if (list.value.length >= total.value) return;
page.value += 1;
loadList(false);
});
</script>
<style scoped>
.min-h-screen {
min-height: 100vh;
}
.bg-gray-100 {
background-color: #f7f8fa;
}
.bg-white {
background-color: #ffffff;
}
.p-15 {
padding: 30rpx;
}
.px-15 {
padding-left: 30rpx;
padding-right: 30rpx;
}
.py-10 {
padding-top: 20rpx;
padding-bottom: 20rpx;
}
.py-5 {
padding-top: 10rpx;
padding-bottom: 10rpx;
}
.px-5 {
padding-left: 10rpx;
padding-right: 10rpx;
}
.mr-10 {
margin-right: 20rpx;
}
.mr-5 {
margin-right: 10rpx;
}
.ml-2 {
margin-left: 10rpx;
}
.mr-2 {
margin-right: 10rpx;
}
.mb-15 {
margin-bottom: 20rpx;
}
.mb-10 {
margin-bottom: 20rpx;
}
.mt-1 {
margin-top: 6rpx;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.flex-1 {
flex: 1;
}
.flex-grow {
flex-grow: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.w-0 {
width: 0;
}
.relative {
position: relative;
}
.border {
border-width: 1px;
border-style: solid;
}
.rounded-full {
border-radius: 9999px;
}
.rounded {
border-radius: 8rpx;
}
.rounded-lg {
border-radius: 12rpx;
}
.shadow-sm {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.text-xs {
font-size: 22rpx;
}
.text-sm {
font-size: 28rpx;
}
.text-base {
font-size: 32rpx;
}
.font-bold {
font-weight: 600;
}
.leading-normal {
line-height: 1.4;
}
.text-orange-500 {
color: #f29e38;
}
.bg-orange-100 {
background-color: #fff8eb;
}
.border-orange-500 {
border-color: #f29e38;
}
.text-gray-600 {
color: #333333;
}
.text-gray-400 {
color: #999999;
}
.text-gray-800 {
color: #1a1a1a;
}
.border-gray-200 {
border-color: #e5e5e5;
}
.text-green-600 {
color: #4b8d5f;
}
.border-green-600 {
border-color: #4b8d5f;
}
.sticky {
position: sticky;
}
.top-0 {
top: 0;
}
.z-10 {
z-index: 10;
}
.w-full {
width: 100%;
}
.whitespace-nowrap {
white-space: nowrap;
}
.inline-block {
display: inline-block;
}
.tag-box {
border-radius: 4rpx;
line-height: 1.2;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.article-card {
background: white;
border-radius: 16rpx;
box-shadow: 0 8rpx 10rpx 0 rgba(60, 169, 145, 0.06);
transition: all 0.3s;
min-height: 188rpx;
padding: 20rpx;
align-items: flex-start;
width: 100%;
box-sizing: border-box;
}
.article-card:active {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.cover {
width: 272rpx;
height: 151rpx;
border-radius: 12rpx;
margin-right: 20rpx;
object-fit: cover;
}
.article-title {
color: #333333;
font-size: 32rpx;
font-weight: 500;
line-height: normal;
}
.article-summary {
max-width: 402rpx;
color: #666666;
text-align: justify;
font-size: 28rpx;
font-weight: 400;
line-height: normal;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
line-height: 1.5;
}
.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;
}
.loading-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #999;
gap: 10rpx;
}
</style>

View File

@ -2,16 +2,25 @@
<view v-if="articles.length" class="article-container">
<view class="flex items-center justify-between">
<view class="module-title">健康宣教</view>
<view class="flex items-center" @click="toList()">
<view v-if="total > 3" class="flex items-center" @click="toMorePage()">
<view class="mr-5 text-base text-gray">更多</view>
<image class="arrow-icon" src="/static/home/arrow-right-gray.png" mode="aspectFit"></image>
</view>
</view>
<view class="mt-10">
<view v-for="(article, index) in articles" :key="article._id"
<view
v-for="(article, index) in articles"
:key="article._id"
class="article-card flex"
:class="{'mb-15': index < articles.length - 1}">
<image class="flex-shrink-0 cover" :src="article.cover || '/static/home/health-education.png'" mode="aspectFill" />
:class="{ 'mb-15': index < articles.length - 1 }"
@click="goToDetail(article)"
>
<image
class="flex-shrink-0 cover"
:src="article.cover || '/static/home/health-education.png'"
mode="aspectFill"
/>
<view class="w-0 flex-grow">
<view class="article-title truncate mb-10">
{{ article.title }}
@ -25,7 +34,7 @@
</view>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { ref, watch } from "vue";
import api from '@/utils/api';
const props = defineProps({
@ -34,34 +43,67 @@ const props = defineProps({
default: () => ({})
}
})
const articles = ref([])
const qrcode = computed(() => {
const qrcodes = props.team && Array.isArray(props.team.qrcodes) ? props.team.qrcodes : [];
return qrcodes[0] || {}
})
const articleIds = computed(() => {
const articles = qrcode.value && Array.isArray(qrcode.value.articles) ? qrcode.value.articles : [];
const ids = articles.map(item => item._id).filter(i => typeof i === 'string' && i.trim());
return qrcode.value.enableAnnounce === 'YES' ? ids : [];
})
const articles = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(3);
const loading = ref(false);
function toList() {
uni.navigateTo({ url: `/pages/article/article-list?corpId=${props.team.corpId}&ids=${articleIds.value.join()}` })
function goToDetail(article) {
if (!article?._id) return;
uni.navigateTo({ url: `/pages/article/article-detail?id=${article._id}` });
}
async function getArticles() {
const res = await api('getArticleByIds', { corpId: props.team.corpId, ids: articleIds.value.join() });
articles.value = res && Array.isArray(res.list) ? res.list : [];
}
watch(articleIds, n => {
if (n.length) {
getArticles()
} else {
articles.value = []
const loadArticles = async (reset = false) => {
const corpId = props.team?.corpId || "";
if (!corpId || loading.value) return;
if (reset) {
page.value = 1;
articles.value = [];
total.value = 0;
}
}, { immediate: true })
loading.value = true;
try {
const params = {
corpId,
page: page.value,
pageSize: pageSize.value,
enable: true,
};
const res = await api("getArticleList", params, false);
const list = res && Array.isArray(res.list) ? res.list : [];
total.value = Number(res?.total) || 0;
if (page.value === 1) articles.value = list;
else articles.value = [...articles.value, ...list];
} catch (err) {
console.error("loadArticles failed:", err);
} finally {
loading.value = false;
}
};
function toMorePage() {
const corpId = props.team?.corpId || "";
if (!corpId) return;
uni.navigateTo({ url: `/pages/article/article-cate-list?corpId=${corpId}` });
}
watch(
() => props.team?.corpId,
async (corpId) => {
if (!corpId) {
articles.value = [];
total.value = 0;
page.value = 1;
pageSize.value = 3;
return;
}
pageSize.value = 3;
await loadArticles(true);
},
{ immediate: true }
);
</script>
<style scoped>
.article-container {
@ -138,4 +180,4 @@ watch(articleIds, n => {
overflow: hidden;
line-height: 1.5;
}
</style>
</style>

View File

@ -47,20 +47,6 @@
>
{{ i.name }}
</view>
<view class="flex items-center mb-5">
<image
v-if="i.sex"
class="sex-icon mr-5"
:src="
i.sex === '男'
? '/static/home/male.svg'
: '/static/home/female.svg'
"
/>
<view class="customer-age text-base leading-normal text-gray">
{{ i.age > 0 ? i.age + "岁" : "" }}
</view>
</view>
</view>
<!-- 选中状态底部条和三角 -->
<view
@ -381,10 +367,6 @@ watch(
margin-top: -8rpx;
}
.customer-age {
color: #999999;
}
.mr-15 {
margin-right: 20rpx;
}

View File

@ -28,6 +28,10 @@ export default [
path: 'pages/article/article-list',
meta: { title: '健康宣教', login: true }
},
{
path: 'pages/article/article-cate-list',
meta: { title: '健康宣教', login: true }
},
{
path: 'pages/health/list',
meta: { title: '健康信息', login: true }