488 lines
12 KiB
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>
|