H5-contact/pages/agreement/index.vue

488 lines
12 KiB
Vue

<template>
<view class="agreement-page">
<scroll-view class="agreement-scroll" scroll-y>
<view class="screen list-screen">
<view v-if="loading" class="state-text">加载中...</view>
<view v-else-if="errorMessage" class="state-text" @click="loadAgreements">{{ errorMessage }}</view>
<view v-else class="list-content">
<view v-if="!agreements.length" class="state-text">暂无协议</view>
<view v-else class="agreement-table">
<view
v-for="item in agreements"
:key="item.id"
class="agreement-row"
hover-class="row-hover"
@click="openPreview(item)"
>
<!-- <view class="version-cell">{{ item.version }}</view> -->
<view class="agreement-cell">
<view class="agreement-info">
<text class="agreement-name">{{ item.title }}</text>
<text v-if="item.date" class="agreement-date">{{ item.date }} 更新</text>
</view>
<text class="view-button">查看</text>
</view>
</view>
</view>
<view v-if="showPagination" class="pagination-bar">
<button
class="page-button"
:class="{ 'page-button-disabled': !canPrevPage || loading }"
:disabled="!canPrevPage || loading"
@click="changePage(currentPage - 1)"
>
上一页
</button>
<text class="page-info">{{ currentPage }} / {{ totalPagesText }}</text>
<button
class="page-button"
:class="{ 'page-button-disabled': !canNextPage || loading }"
:disabled="!canNextPage || loading"
@click="changePage(currentPage + 1)"
>
下一页
</button>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { getContractList } from '@/api/contract.js';
import { refreshCurrentWebviewToken } from '@/utils/token.js';
export default {
data() {
return {
agreements: [],
loading: false,
errorMessage: '',
currentPage: 1,
pageSize: 10,
total: null,
hasNextPage: false
};
},
computed: {
totalPages() {
if (this.total === null) return this.currentPage;
return Math.max(1, Math.ceil(this.total / this.pageSize));
},
totalPagesText() {
if (this.total !== null) return String(this.totalPages);
return this.hasNextPage ? this.currentPage + '+' : String(this.currentPage);
},
canPrevPage() {
return this.currentPage > 1;
},
canNextPage() {
if (this.total !== null) return this.currentPage < this.totalPages;
return this.hasNextPage;
},
showPagination() {
return this.currentPage > 1 || this.canNextPage || (this.total !== null && this.total > this.pageSize);
}
},
onLoad(options = {}) {
refreshCurrentWebviewToken(options.token ? '?token=' + encodeURIComponent(options.token) : '');
uni.setNavigationBarTitle({
title: '旧版协议'
});
this.loadAgreements(1);
},
methods: {
async loadAgreements(page = this.currentPage) {
if (this.loading) return;
const nextPage = Math.max(1, Number(page) || 1);
this.loading = true;
this.errorMessage = '';
try {
const response = await getContractList({
page: nextPage,
limit: this.pageSize
});
const list = this.normalizeList(response);
const pagination = this.resolvePagination(response, list.length, nextPage);
this.currentPage = nextPage;
this.agreements = list;
this.total = pagination.total;
this.hasNextPage = pagination.hasNextPage;
} catch (error) {
this.errorMessage = this.getErrorMessage(error, '协议加载失败,点击重试');
} finally {
this.loading = false;
}
},
normalizeList(response) {
const payload = this.unwrapPayload(response);
const list = this.pickList(payload);
return list
.map(item => this.normalizeAgreementItem(item))
.filter(item => item.id !== '');
},
unwrapPayload(response) {
if (response && typeof response === 'object' && response.data !== undefined) {
return response.data;
}
return response;
},
pickList(payload) {
if (Array.isArray(payload)) return payload;
if (!payload || typeof payload !== 'object') return [];
const candidates = [
payload.list,
payload.data,
payload.data && payload.data.list,
payload.data && payload.data.rows,
payload.data && payload.data.records,
payload.rows,
payload.records,
payload.result,
payload.result && payload.result.list
];
for (let index = 0; index < candidates.length; index += 1) {
if (Array.isArray(candidates[index])) {
return candidates[index];
}
}
return [];
},
resolvePagination(response, listLength, page) {
const sources = this.collectPaginationSources(response);
const total = this.pickNumber(sources, [
'total',
'count',
'total_count',
'totalCount',
'total_num',
'totalNum'
]);
const responsePageSize =
this.pickNumber(sources, ['limit', 'pageSize', 'page_size', 'per_page', 'perPage']) || this.pageSize;
const explicitHasNext = this.pickValue(sources, ['has_more', 'hasMore', 'has_next', 'hasNext']);
let hasNextPage = false;
if (total !== null) {
hasNextPage = page < Math.max(1, Math.ceil(total / responsePageSize));
} else if (explicitHasNext !== undefined) {
hasNextPage = explicitHasNext === true || explicitHasNext === 1 || explicitHasNext === '1';
} else {
hasNextPage = listLength >= responsePageSize;
}
return {
total,
hasNextPage
};
},
collectPaginationSources(response) {
const sources = [];
const stack = [response];
const seen = [];
while (stack.length && sources.length < 12) {
const item = stack.shift();
if (!item || typeof item !== 'object' || seen.indexOf(item) !== -1) continue;
seen.push(item);
sources.push(item);
['data', 'result', 'page', 'pagination', 'meta'].forEach(key => {
if (item[key] && typeof item[key] === 'object') {
stack.push(item[key]);
}
});
}
return sources;
},
pickNumber(sources, keys) {
const value = this.pickValue(sources, keys);
if (value === undefined || value === null || value === '') return null;
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : null;
},
pickValue(sources, keys) {
for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
const source = sources[sourceIndex];
for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
const key = keys[keyIndex];
if (Object.prototype.hasOwnProperty.call(source, key)) {
return source[key];
}
}
}
return undefined;
},
normalizeAgreementItem(item) {
item = item || {};
const id = this.toText(item.id || item.contract_id || item.agreement_id);
const title = this.toText(
item.title ||
item.name ||
item.contract_name ||
item.agreement_name ||
item.content_name || '未命名'
);
const version = this.toText(
item.version ||
item.version_name ||
item.contract_version ||
item.edition ||
item.v ||
(id ? 'id' + id : '')
);
const date = this.formatDate(
item.date ||
item.release_time ||
item.publish_time ||
item.create_time ||
item.add_time ||
item.updated_at ||
item.update_time
);
return {
id,
title,
version,
date
};
},
formatDate(value) {
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'number') {
const timestamp = value > 100000000000 ? value : value * 1000;
const date = new Date(timestamp);
if (!Number.isNaN(date.getTime())) {
return this.padDate(date);
}
}
const text = this.toText(value);
if (/^\d+$/.test(text)) {
return this.formatDate(Number(text));
}
return text.split(' ')[0].replace(/-/g, '年').replace(/^(\d{4})年(\d{2})年(\d{2})$/, '$1年$2月$3日');
},
padDate(date) {
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return date.getFullYear() + '年' + month + '月' + day + '日';
},
toText(value) {
if (value === undefined || value === null) return '';
return String(value).trim();
},
getErrorMessage(error, defaultMessage) {
if (error && error.msg) return error.msg;
if (error && error.message) return error.message;
if (typeof error === 'string') return error;
return defaultMessage;
},
changePage(page) {
if (page < 1 || this.loading) return;
if (page > this.currentPage && !this.canNextPage) return;
this.loadAgreements(page);
},
openPreview(item) {
if (!item.id) {
uni.showToast({
title: '协议ID为空',
icon: 'none'
});
return;
}
uni.navigateTo({
url: `/pages/agreement/preview/index?id=${encodeURIComponent(item.id)}`
});
}
}
};
</script>
<style scoped>
page {
height: 100%;
overflow: hidden;
background: #ffffff;
}
.agreement-page {
width: 100%;
height: 100%;
min-height: 0;
display: flex;
overflow: hidden;
background: #ffffff;
color: #111111;
}
.agreement-scroll {
flex: 1;
width: 100%;
height: 100%;
}
.screen {
background: #ffffff;
}
.list-screen {
display: flex;
box-sizing: border-box;
min-height: 100%;
flex-direction: column;
overflow: visible;
padding: 16px 16px calc(14px + env(safe-area-inset-bottom));
}
.list-content {
display: flex;
flex: 1 1 auto;
min-height: 0;
flex-direction: column;
overflow: visible;
}
.agreement-table {
display: flex;
flex: 0 0 auto;
min-height: 0;
flex-direction: column;
overflow: visible;
width: 100%;
border: 1px solid #c9c9c9;
border-bottom: none;
background: #fff0e2;
}
.state-text {
padding: 28px 0;
color: #666666;
font-size: 14px;
line-height: 1.5;
text-align: center;
}
.agreement-row {
display: flex;
flex: 0 0 auto;
min-height: 54px;
border-bottom: 1px solid #c9c9c9;
}
.row-hover {
opacity: 0.78;
}
.version-cell {
display: flex;
flex: 0 0 56px;
align-items: center;
justify-content: center;
border-right: 1px solid #c9c9c9;
color: #111111;
font-size: 14px;
line-height: 1.45;
}
.agreement-cell {
display: flex;
flex: 1;
min-width: 0;
align-items: center;
justify-content: space-between;
padding: 0;
background: #fff0e2;
color: #171717;
}
.agreement-info {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
justify-content: center;
padding: 7px 10px;
}
.agreement-name,
.agreement-date {
display: block;
overflow: hidden;
font-size: 28rpx;
line-height: 1.5;
text-overflow: ellipsis;
white-space: nowrap;
}
.agreement-date {
font-size: 24rpx;
color: gray;
margin-top: 14rpx;
}
.view-button {
display: flex;
flex: 0 0 48px;
align-self: stretch;
align-items: center;
justify-content: center;
border-left: 1px solid #c9c9c9;
color: #1677ff;
font-size: 14px;
line-height: 1.45;
text-decoration: underline;
}
.pagination-bar {
display: flex;
flex: 0 0 44px;
align-items: center;
justify-content: center;
padding: 14px 0 2px;
}
.page-button {
width: 74px;
height: 32px;
margin: 0;
padding: 0;
border: 1px solid #ec0208;
border-radius: 16px;
background: #ffffff;
color: #ec0208;
font-size: 13px;
line-height: 30px;
}
.page-button::after {
border: 0;
}
.page-button-disabled {
border-color: #d8d8d8;
color: #a6a6a6;
background: #f7f7f7;
}
.page-info {
min-width: 68px;
height: 32px;
margin: 0 10px;
padding: 0 8px;
border-radius: 16px;
background: #f5f7fa;
color: #333333;
font-size: 13px;
line-height: 32px;
text-align: center;
}
</style>