You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
crmeb/app/pages/supply_chain/notice/index.vue

510 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="notice-list-page">
<view class="content">
<!-- 搜索与筛选 -->
<view class="search-section">
<view class="search-box">
<input v-model="query.noticeTitle" type="text" placeholder="请输入通知标题" />
<view class="search-btn" @click="refreshList">
搜索
</view>
</view>
<view class="filter-row">
<text class="filter-label">通知类型</text>
<picker
mode="selector"
:range="noticeTypeOptions"
range-key="label"
:value="selectedNoticeTypeIndex"
@change="onNoticeTypeChange"
>
<view class="filter-picker">
<text class="picker-text">{{ selectedNoticeTypeLabel }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="filter-row" style="margin-top: 20rpx;">
<text class="filter-label">阅读状态</text>
<picker
mode="selector"
:range="noticeReadOptions"
range-key="label"
:value="selectedNoticeReadIndex"
@change="onNoticeReadChange"
>
<view class="filter-picker">
<text class="picker-text">{{ selectedNoticeReadLabel }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
</view>
<!-- 列表 -->
<scroll-view scroll-y class="list-scroll" @scrolltolower="loadMore">
<view class="empty" v-if="!loading && records.length === 0">
<text class="iconfont icon-wushuju"></text>
<text class="text">暂无通知公告</text>
</view>
<view
v-for="item in records"
:key="item.noticeId"
class="record-card"
@click="openDetail(item.noticeId)"
>
<view class="card-header">
<view class="left">
<view class="title-row">
<text class="type line1">{{ item.noticeTitle || '未命名通知' }}</text>
</view>
</view>
<view class="time-wrap">
<text class="time">{{ item.noticeTime }}</text>
<view class="read-badge" v-if="isUnread(item.isRead)">
<view class="red-dot"></view>
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">通知类型:</text>
<text class="value">{{ dict.getLabel('sys_notice_type', item.noticeType) || item.noticeType || '—' }}</text>
</view>
<view class="row">
<text class="label">通知范围:</text>
<text class="value">{{ item.noticeScope || '—' }}</text>
</view>
<view class="row">
<text class="label">通知单位:</text>
<text class="value">{{ item.draftDeptName || '—' }}</text>
</view>
<view class="preview" v-if="item.noticeContent">
{{ contentPreview(item.noticeContent) }}
</view>
</view>
<view class="card-footer">
<view class="detail-btn" @click.stop="openDetail(item.noticeId)">查看详情</view>
</view>
</view>
<view class="load-more" v-if="loading">加载中...</view>
<view class="load-more" v-else-if="finished && records.length > 0">已加载全部</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { pubnoticeListApi } from '@/api/pubnotice.js';
export default {
dicts: ['sys_notice_type'],
data() {
return {
query: {
noticeTitle: '',
noticeType: '',
// 1=已读0=未读,''=全部
isRead: '',
},
records: [],
page: 1,
limit: 10,
loading: false,
finished: false,
};
},
onLoad() {
this.refreshList();
},
onPullDownRefresh() {
this.refreshList();
},
computed: {
noticeTypeOptions() {
const dictList = this.dict.get('sys_notice_type') || [];
const options = dictList.map(item => ({
label: item.dictLabel,
value: item.dictValue,
}));
return [{ label: '全部', value: '' }].concat(options);
},
selectedNoticeTypeIndex() {
const index = this.noticeTypeOptions.findIndex(item => item.value === this.query.noticeType);
return index >= 0 ? index : 0;
},
selectedNoticeTypeLabel() {
const current = this.noticeTypeOptions[this.selectedNoticeTypeIndex];
return current ? current.label : '全部';
},
noticeReadOptions() {
// 后端约定:空=全部0=未读1=已读
return [
{ label: '全部', value: '' },
{ label: '未读', value: 0 },
{ label: '已读', value: 1 },
];
},
selectedNoticeReadIndex() {
const index = this.noticeReadOptions.findIndex(item => item.value === this.query.isRead);
return index >= 0 ? index : 0;
},
selectedNoticeReadLabel() {
const current = this.noticeReadOptions[this.selectedNoticeReadIndex];
return current ? current.label : '全部';
},
},
methods: {
onNoticeTypeChange(e) {
const index = Number(e?.detail?.value);
const current = this.noticeTypeOptions[index];
this.query.noticeType = current ? current.value : '';
this.refreshList();
},
onNoticeReadChange(e) {
const index = Number(e?.detail?.value);
const current = this.noticeReadOptions[index];
this.query.isRead = current ? current.value : '';
this.refreshList();
},
async refreshList() {
this.page = 1;
this.records = [];
this.finished = false;
await this.fetchList();
uni.stopPullDownRefresh && uni.stopPullDownRefresh();
},
async loadMore() {
if (this.loading || this.finished) return;
this.page += 1;
await this.fetchList();
},
buildParams() {
const params = {
page: this.page,
limit: this.limit,
delFlag: '1',
};
const noticeTitle = (this.query.noticeTitle || '').trim();
if (noticeTitle) params.noticeTitle = noticeTitle;
if (this.query.noticeType) {
params.noticeType = this.query.noticeType;
// 兼容后端仍使用 type 字段筛选的场景
params.type = this.query.noticeType;
}
if (this.query.isRead !== '' && this.query.isRead !== undefined && this.query.isRead !== null) {
params.isRead = this.query.isRead;
}
return params;
},
async fetchList() {
this.loading = true;
try {
const params = this.buildParams();
const res = await pubnoticeListApi(params);
const data = res?.data || {};
const list = data?.list || [];
if (this.page === 1) {
this.records = list;
} else {
this.records = this.records.concat(list);
}
const total = data?.total ?? data?.count ?? res?.total ?? res?.count;
if (!list.length) {
this.finished = true;
} else if (typeof total === 'number' && total <= this.records.length) {
this.finished = true;
}
} catch (e) {
uni.showToast({
title: typeof e === 'string' ? e : '获取通知公告失败',
icon: 'none',
});
} finally {
this.loading = false;
}
},
openDetail(noticeId) {
if (!noticeId) return;
// 点击列表进入详情:只更新列表本地状态(乐观展示)
const current = this.records?.find((r) => r.noticeId === noticeId);
if (current) {
// Vue2 响应式兜底:属性不存在时用 $set
if (Object.prototype.hasOwnProperty.call(current, 'isRead')) {
current.isRead = 1;
} else if (this.$set) {
this.$set(current, 'isRead', 1);
} else {
current.isRead = 1;
}
}
uni.navigateTo({
url: `/pages/supply_chain/notice_detail/index?noticeId=${noticeId}`,
});
},
contentPreview(content) {
if (content === undefined || content === null) return '';
const s = String(content)
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!s) return '';
return s.length > 80 ? s.slice(0, 80) + '...' : s;
},
getReadStatusText(isRead) {
if (isRead === 1 || isRead === '1') return '已读';
if (isRead === 0 || isRead === '0') return '未读';
return '';
},
getReadStatusClass(isRead) {
if (isRead === 1 || isRead === '1') return 'read-status--read';
if (isRead === 0 || isRead === '0') return 'read-status--unread';
return '';
},
isUnread(isRead) {
return isRead === 0 || isRead === '0';
},
},
};
</script>
<style lang="scss">
.notice-list-page {
.content {
padding: 30rpx;
}
.search-section {
margin-bottom: 20rpx;
.search-box {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.06);
padding: 14rpx 16rpx;
margin-bottom: 20rpx;
input {
flex: 1;
height: 64rpx;
font-size: 26rpx;
color: #333;
}
.search-btn {
height: 64rpx;
padding: 0 18rpx;
background-color: #409eff;
color: #fff;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 600;
}
}
.filter-row {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.06);
padding: 0 20rpx;
height: 72rpx;
.filter-label {
font-size: 26rpx;
color: #666;
}
.filter-picker {
display: flex;
align-items: center;
max-width: 420rpx;
.picker-text {
font-size: 26rpx;
color: #409eff;
max-width: 360rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.picker-arrow {
margin-left: 10rpx;
font-size: 18rpx;
color: #999;
}
}
}
}
.list-scroll {
max-height: calc(100vh - 310rpx);
}
.record-card {
background-color: #fff;
border-radius: 10rpx;
padding: 24rpx 26rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.06);
position: relative;
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16rpx;
.left {
flex: 1;
min-width: 0;
}
.title-row {
margin-bottom: 10rpx;
}
.type {
font-size: 28rpx;
font-weight: 700;
color: #333;
max-width: 520rpx;
}
.time {
font-size: 22rpx;
color: #999;
white-space: nowrap;
}
.time-wrap {
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
flex-shrink: 0;
align-self: center;
}
}
//
.read-badge {
display: flex;
align-items: center;
justify-content: center;
margin-left: 12rpx;
.red-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background-color: #f56c6c;
}
}
.card-body {
.row {
display: flex;
margin-bottom: 10rpx;
.label {
width: 170rpx;
font-size: 24rpx;
color: #666;
}
.value {
flex: 1;
font-size: 24rpx;
color: #333;
min-width: 0;
}
}
.preview {
margin-top: 16rpx;
font-size: 24rpx;
color: #999;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.card-footer {
margin-top: 20rpx;
display: flex;
justify-content: flex-end;
.detail-btn {
padding: 8rpx 16rpx;
background-color: #409eff;
color: #fff;
border-radius: 16rpx;
font-size: 20rpx;
}
}
}
.empty {
margin-top: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
color: #999;
.iconfont {
font-size: 80rpx;
margin-bottom: 16rpx;
}
.text {
font-size: 24rpx;
}
}
.load-more {
text-align: center;
padding: 20rpx 0 10rpx;
font-size: 22rpx;
color: #999;
}
.line1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>