统一排行榜对齐星雀UI,页面资源后台配置实时刷新

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-27 01:26:11 +08:00
parent 1ab2e2080a
commit 21112173f2
292 changed files with 64010 additions and 81 deletions

View File

@@ -0,0 +1,317 @@
// components/fakuan-list/fakuan-list.js
import request from '../../../../utils/request.js';
const app = getApp();
const MEIYE_TIAOSHU = 5;
Component({
properties: {
status: { type: Number, value: 1, observer: 'reload' },
searchDingdan: { type: String, value: '', observer: 'reload' },
trigger: { type: Number, value: 0, observer: 'reload' }
},
data: {
list: [],
page: 1,
hasMore: true,
loading: false,
loadingMore: false,
ossImageUrl: app.globalData.ossImageUrl || '',
// 详情弹窗
showDetail: false,
detailItem: null,
// 申诉弹窗
showShensu: false,
shensuLiyou: '',
shensuTupian: [],
uploading: false,
uploadProgress: { current: 0, total: 0 },
uploadProgressWidth: '0%',
// 支付弹窗
showPay: false,
payFadan: null
},
lifetimes: {
attached() { this.reload(); }
},
methods: {
// ==================== 数据加载 ====================
reload() {
this.setData({ page: 1, list: [], hasMore: true });
this.loadData();
},
async loadData(isLoadMore = false) {
// 防止重复请求
if (this.data.loading || this.data.loadingMore) return;
if (!isLoadMore) {
this.setData({ loading: true });
} else {
// 加载更多时,如果后端已经告知没有更多数据,不发送请求
if (!this.data.hasMore) {
wx.showToast({ title: '没有更多了', icon: 'none', duration: 1000 });
return;
}
this.setData({ loadingMore: true });
}
try {
const res = await request({
url: '/yonghu/dsfklbhq',
method: 'POST',
data: {
page: this.data.page,
page_size: MEIYE_TIAOSHU,
zhuangtai: this.data.status,
sousuo_dingdan_id: this.data.searchDingdan
}
});
if (res.statusCode === 200 && res.data.code === 0) {
const data = res.data.data;
const newList = isLoadMore ? this.data.list.concat(data.list || []) : (data.list || []);
this.setData({
list: newList,
hasMore: data.has_more || false, // 以后端返回为准
page: isLoadMore ? this.data.page + 1 : 2, // 首次加载后页码为2
loading: false,
loadingMore: false
});
} else {
wx.showToast({ title: res.data?.msg || '加载失败', icon: 'none' });
this.setData({ loading: false, loadingMore: false });
}
} catch (e) {
wx.showToast({ title: '网络请求失败', icon: 'none' });
this.setData({ loading: false, loadingMore: false });
}
},
loadMore() {
if (this.data.list.length === 0) return;
this.loadData(true);
},
// ==================== 详情弹窗 ====================
openDetail(e) {
const item = e.currentTarget.dataset.item;
this.setData({ showDetail: true, detailItem: item });
},
closeDetail() {
this.setData({ showDetail: false, detailItem: null });
},
// ==================== 申诉逻辑 ====================
openShensu() {
const item = this.data.detailItem;
if (item.bohuiliyou) {
wx.showToast({ title: '该罚单申诉已被驳回', icon: 'none' });
return;
}
const existingImgs = (item.shensu_tupian || []).map(url => this.getFullUrl(url));
this.setData({
showShensu: true,
shensuLiyou: item.shensuliyou || '',
shensuTupian: existingImgs,
uploading: false,
uploadProgress: { current: 0, total: 0 },
uploadProgressWidth: '0%'
});
},
closeShensu() {
this.setData({ showShensu: false });
},
onShensuInput(e) {
this.setData({ shensuLiyou: e.detail.value.slice(0, 500) });
},
chooseImage() {
const remain = 9 - this.data.shensuTupian.length;
if (remain <= 0) return;
wx.chooseMedia({
count: remain,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const newImgs = [...this.data.shensuTupian, ...res.tempFiles.map(f => f.tempFilePath)];
this.setData({ shensuTupian: newImgs.slice(0, 9) });
}
});
},
deleteShensuImg(e) {
const arr = this.data.shensuTupian;
arr.splice(e.currentTarget.dataset.index, 1);
this.setData({ shensuTupian: arr });
},
previewImage(e) {
wx.previewImage({ current: e.currentTarget.dataset.url, urls: [e.currentTarget.dataset.url] });
},
previewLocalImg(e) {
const idx = e.currentTarget.dataset.index;
wx.previewImage({ current: this.data.shensuTupian[idx], urls: this.data.shensuTupian });
},
// ==================== 提交流程 ====================
async submitShensu() {
if (!this.data.shensuLiyou.trim()) {
wx.showToast({ title: '请输入申诉理由', icon: 'none' });
return;
}
wx.showLoading({ title: '提交中...' });
try {
const newLocalPaths = this.data.shensuTupian.filter(path => !path.startsWith('http'));
let finalUrls = [];
if (newLocalPaths.length > 0) {
finalUrls = await this.piliangShangchuanShensuTupian(newLocalPaths);
if (!finalUrls) {
wx.hideLoading();
return;
}
}
const oldRemoteRelative = this.data.shensuTupian
.filter(path => path.startsWith('http'))
.map(url => url.replace(this.data.ossImageUrl, '').replace(/^\//, ''));
const allUrls = oldRemoteRelative.concat(finalUrls);
await this.doSubmitShensu(allUrls);
} catch (err) {
console.error('提交申诉失败', err);
wx.showToast({ title: err.message || '提交失败', icon: 'none' });
} finally {
wx.hideLoading();
}
},
async doSubmitShensu(tupianUrls) {
const res = await request({
url: '/yonghu/fkss',
method: 'POST',
data: {
fadan_id: this.data.detailItem.id,
shensu_liyou: this.data.shensuLiyou,
shensu_tupian_urls: tupianUrls
}
});
if (res.statusCode === 200 && res.data.code === 0) {
wx.showToast({ title: '申诉已提交', icon: 'success' });
this.setData({ showShensu: false, showDetail: false });
this.reload();
} else {
throw new Error(res.data?.msg || '提交失败');
}
},
// ==================== 图片上传到COS ====================
async piliangShangchuanShensuTupian(localPaths) {
const total = localPaths.length;
if (!total) return [];
this.setData({ uploading: true, uploadProgress: { current: 0, total: total }, uploadProgressWidth: '0%' });
let tokenData;
try {
const credRes = await request({
url: '/dingdan/dsscpz',
method: 'POST',
data: {
dingdan_id: this.data.detailItem.guanliandingdan_id || '',
fadan_id: this.data.detailItem.id,
yongtu: 'fakuan'
}
});
if (credRes.data.code !== 0) throw new Error(credRes.data.msg || '凭证获取失败');
tokenData = credRes.data.data;
} catch (e) {
wx.showToast({ title: '获取上传凭证失败', icon: 'none' });
this.setData({ uploading: false });
return null;
}
const COS = require('../../../../utils/cos-wx-sdk-v5.js');
const credentials = tokenData.credentials || tokenData;
const cos = new COS({
SimpleUploadMethod: 'putObject',
getAuthorization: (options, callback) => {
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
SecurityToken: credentials.sessionToken || '',
StartTime: tokenData.startTime,
ExpiredTime: tokenData.expiredTime
});
}
});
const bucket = tokenData.bucket || 'julebu-1361527063';
const region = tokenData.region || 'ap-shanghai';
const yonghuid = app.globalData.userInfo?.yonghuid || wx.getStorageSync('uid') || '0000000';
const fadanId = this.data.detailItem.id;
const keys = [];
for (let i = 0; i < total; i++) {
keys.push(`fakuan/dashoufakuan/dashoufakuanshensu/${yonghuid}_${fadanId}_${Date.now() + i}.jpg`);
}
for (let i = 0; i < total; i++) {
try {
await new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => resolve(), 2000);
cos.uploadFile({
Bucket: bucket,
Region: region,
Key: keys[i],
FilePath: localPaths[i],
onProgress: (info) => {
if (Math.round(info.percent * 100) === 100) {
clearTimeout(timeoutId);
resolve();
}
}
}, (err) => {
clearTimeout(timeoutId);
if (err) reject(err);
else resolve();
});
});
} catch (err) {
wx.showToast({ title: `${i + 1} 张上传失败`, icon: 'none' });
this.setData({ uploading: false });
return null;
}
const done = i + 1;
this.setData({
uploadProgress: { current: done, total: total },
uploadProgressWidth: `${((done / total) * 100).toFixed(0)}%`
});
}
this.setData({ uploading: false });
return keys;
},
// ==================== 缴纳支付 ====================
startPay() {
this.setData({
showDetail: false,
showPay: true,
payFadan: this.data.detailItem
});
},
onPayClose() {
this.setData({ showPay: false, payFadan: null });
},
onPaySuccess() {
this.setData({ showPay: false, payFadan: null });
this.reload();
},
// ==================== 工具方法 ====================
getFullUrl(url) {
if (!url) return '';
if (url.startsWith('http')) return url;
return this.data.ossImageUrl + url;
}
}
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"fakuan-pay": "/pages/cfss/components/fakuan-pay/fakuan-pay"
}
}

View File

@@ -0,0 +1,116 @@
<!-- components/fakuan-list/fakuan-list.wxml -->
<view class="container">
<scroll-view scroll-y class="scroll" bindscrolltolower="loadMore">
<view wx:if="{{ loading && list.length === 0 }}" class="tip">加载中...</view>
<block wx:for="{{ list }}" wx:key="id">
<view class="card" bindtap="openDetail" data-item="{{ item }}">
<view class="card-head">
<text class="time">{{ item.create_time }}</text>
<text class="tag {{ item.status_class }}">{{ item.display_status }}</text>
</view>
<text class="info">金额:¥{{ item.fakuanjine }}</text>
<text class="info">理由:{{ item.chufaliyou }}</text>
<text class="info" wx:if="{{ item.guanliandingdan_id }}">订单:{{ item.guanliandingdan_id }}</text>
</view>
</block>
<!-- ✅ 按钮始终显示,只要列表不为空且没有正在加载 -->
<view wx:if="{{ list.length > 0 && !loadingMore }}" class="load-more-btn" bindtap="loadMore">
<text>点击获取更多</text>
</view>
<view wx:if="{{ loadingMore }}" class="tip">加载中...</view>
<view wx:if="{{ !hasMore && list.length > 0 }}" class="tip">—— 没有更多了 ——</view>
<view wx:if="{{ !loading && list.length === 0 }}" class="empty">暂无罚款记录</view>
</scroll-view>
<!-- 详情弹窗 -->
<view wx:if="{{ showDetail }}" class="modal-mask">
<view class="modal-panel">
<view class="modal-head">
<text class="modal-title">罚款详情</text>
<view class="modal-close" bindtap="closeDetail">✕</view>
</view>
<scroll-view scroll-y class="modal-body">
<view class="row"><text class="lbl">罚款金额</text><text class="val strong">¥{{ detailItem.fakuanjine }}</text></view>
<view class="row"><text class="lbl">当前状态</text><text class="tag {{ detailItem.status_class }}">{{ detailItem.display_status }}</text></view>
<view class="row"><text class="lbl">处罚理由</text><text class="val">{{ detailItem.chufaliyou }}</text></view>
<view wx:if="{{ detailItem.guanliandingdan_id }}" class="row"><text class="lbl">关联订单</text><text class="val">{{ detailItem.guanliandingdan_id }}</text></view>
<view wx:if="{{ detailItem.bohuiliyou }}" class="row"><text class="lbl">驳回理由</text><text class="val">{{ detailItem.bohuiliyou }}</text></view>
<view wx:if="{{ detailItem.shensuliyou }}" class="row"><text class="lbl">申诉理由</text><text class="val">{{ detailItem.shensuliyou }}</text></view>
<!-- 证据图片 -->
<view wx:if="{{ detailItem.zhengju_tupian && detailItem.zhengju_tupian.length }}" class="section">
<text class="sec-title">证据图片</text>
<view class="img-grid">
<block wx:for="{{ detailItem.zhengju_tupian }}" wx:key="index">
<view class="img-box" bindtap="previewImage" data-url="{{ ossImageUrl + item }}">
<image class="img" src="{{ ossImageUrl + item }}" mode="aspectFill" />
</view>
</block>
</view>
</view>
<!-- 申诉图片 -->
<view wx:if="{{ detailItem.shensu_tupian && detailItem.shensu_tupian.length }}" class="section">
<text class="sec-title">申诉图片</text>
<view class="img-grid">
<block wx:for="{{ detailItem.shensu_tupian }}" wx:key="index">
<view class="img-box" bindtap="previewImage" data-url="{{ ossImageUrl + item }}">
<image class="img" src="{{ ossImageUrl + item }}" mode="aspectFill" />
</view>
</block>
</view>
</view>
</scroll-view>
<view class="modal-foot">
<view wx:if="{{ detailItem.zhuangtai === 1 && !detailItem.bohuiliyou }}" class="btn primary" bindtap="openShensu">申诉</view>
<view wx:if="{{ detailItem.zhuangtai === 1 || detailItem.zhuangtai === 3 }}" class="btn primary" bindtap="startPay">立即缴纳</view>
<view class="btn default" bindtap="closeDetail">关闭</view>
</view>
</view>
</view>
<!-- 申诉弹窗 -->
<view wx:if="{{ showShensu }}" class="modal-mask">
<view class="modal-panel">
<view class="modal-head">
<text class="modal-title">提交申诉</text>
<view class="modal-close" bindtap="closeShensu">✕</view>
</view>
<scroll-view scroll-y class="modal-body">
<view class="form-group">
<text class="sec-title">申诉理由</text>
<textarea class="textarea" placeholder="请输入申诉理由" value="{{ shensuLiyou }}" bindinput="onShensuInput" maxlength="500" />
<text class="word-count">{{ shensuLiyou.length }}/500</text>
</view>
<view class="form-group">
<text class="sec-title">上传凭证最多9张</text>
<view class="img-grid">
<block wx:for="{{ shensuTupian }}" wx:key="index">
<view class="img-box">
<image class="img" src="{{ item }}" mode="aspectFill" bindtap="previewLocalImg" data-index="{{ index }}" />
<view class="img-del" catchtap="deleteShensuImg" data-index="{{ index }}">✕</view>
</view>
</block>
<view wx:if="{{ shensuTupian.length < 9 }}" class="img-add" bindtap="chooseImage">+</view>
</view>
<view wx:if="{{ uploading }}" class="progress-box">
<view class="progress-bar"><view class="progress-inner" style="width: {{ uploadProgressWidth }}"></view></view>
<text class="progress-text">{{ uploadProgress.current }}/{{ uploadProgress.total }}</text>
</view>
</view>
</scroll-view>
<view class="modal-foot">
<view class="btn default" bindtap="closeShensu">取消</view>
<view class="btn primary" bindtap="submitShensu">提交申诉</view>
</view>
</view>
</view>
<!-- 支付组件(缴纳罚款) -->
<fakuan-pay
visible="{{ showPay }}"
fadan="{{ payFadan }}"
bind:close="onPayClose"
bind:success="onPaySuccess"
/>
</view>

View File

@@ -0,0 +1,77 @@
/* 完全复用积分组件的样式 */
page { background: #f5f5f5; }
.container { width: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
.scroll { width: 100%; box-sizing: border-box; padding: 20rpx 24rpx; }
.tip { text-align: center; color: #999; font-size: 24rpx; padding: 30rpx 0; }
.empty { display: flex; flex-direction: column; align-items: center; padding-top: 200rpx; color: #999; font-size: 28rpx; }
.card { width: 100%; box-sizing: border-box; background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
.card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
.time { font-size: 24rpx; color: #999; }
.tag { font-size: 20rpx; padding: 2rpx 14rpx; border-radius: 16rpx; display: inline-block; }
/* 状态标签颜色(与积分组件完全相同) */
.zhuangtai-daijiaona { background: #FFF3E0; color: #E65100; } /* 待缴纳 */
.zhuangtai-yijiaona { background: #E8F5E9; color: #2E7D32; } /* 已缴纳 */
.zhuangtai-shensuzhong { background: #E3F2FD; color: #1565C0; } /* 申诉中 */
.zhuangtai-yibohui { background: #F3E5F5; color: #7B1FA2; } /* 申诉成功 */
.info { display: block; font-size: 26rpx; color: #333; margin-top: 8rpx; }
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999; display: flex; align-items: flex-end; justify-content: center; }
.modal-panel { width: 100%; box-sizing: border-box; max-height: 85vh; background: #fff; border-radius: 24rpx 24rpx 0 0; display: flex; flex-direction: column; overflow: hidden; }
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 30rpx 16rpx; border-bottom: 1px solid #eee; }
.modal-title { font-size: 32rpx; font-weight: 600; }
.modal-close { width: 44rpx; height: 44rpx; border-radius: 50%; background: #f5f5f5; display: flex; align-items: center; justify-content: center; font-size: 26rpx; color: #888; }
.modal-body { width: 100%; box-sizing: border-box; padding: 20rpx 30rpx; overflow-y: auto; }
.row { display: flex; margin-bottom: 16rpx; }
.lbl { width: 150rpx; font-size: 26rpx; color: #888; flex-shrink: 0; }
.val { flex: 1; font-size: 26rpx; color: #333; word-break: break-all; }
.strong { font-weight: 600; color: #E53935; }
.section { margin: 20rpx 0; }
.sec-title { font-size: 26rpx; color: #888; margin-bottom: 12rpx; display: block; }
.img-grid { width: 100%; box-sizing: border-box; display: flex; flex-wrap: wrap; gap: 12rpx; }
.img-box { position: relative; width: calc((100% - 24rpx) / 3); height: 0; padding-bottom: calc((100% - 24rpx) / 3); }
.img-box .img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border-radius: 10rpx; background: #f0f0f0; }
.img-del { position: absolute; top: -6rpx; right: -6rpx; width: 34rpx; height: 34rpx; background: #e53935; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 18rpx; z-index: 2; }
.img-add { width: calc((100% - 24rpx) / 3); height: 0; padding-bottom: calc((100% - 24rpx) / 3); border: 2rpx dashed #ccc; border-radius: 10rpx; display: flex; align-items: center; justify-content: center; font-size: 48rpx; color: #aaa; position: relative; box-sizing: border-box; }
.form-group { margin-bottom: 24rpx; }
.textarea { width: 100%; box-sizing: border-box; height: 180rpx; background: #f9f9f9; border-radius: 10rpx; padding: 16rpx; font-size: 26rpx; border: 1px solid #eee; }
.word-count { text-align: right; font-size: 22rpx; color: #bbb; margin-top: 6rpx; }
.progress-box { margin-top: 12rpx; }
.progress-bar { height: 10rpx; background: #eee; border-radius: 5rpx; }
.progress-inner { height: 100%; background: #2e7d32; border-radius: 5rpx; }
.progress-text { font-size: 22rpx; color: #2e7d32; text-align: right; margin-top: 4rpx; }
.modal-foot { width: 100%; box-sizing: border-box; display: flex; padding: 16rpx 30rpx 30rpx; gap: 20rpx; border-top: 1px solid #eee; }
.btn { flex: 1; text-align: center; padding: 22rpx 0; border-radius: 16rpx; font-size: 28rpx; font-weight: 600; }
.primary { background: #2e7d32; color: #fff; }
.default { background: #f0f0f0; color: #555; }
.btn:active { opacity: 0.8; }
/* ===== 获取更多按钮 ===== */
.load-more-btn {
width: 80%;
margin: 20rpx auto 30rpx;
padding: 22rpx 0;
text-align: center;
background: #ffffff;
border: 2rpx solid #e0e0e0;
border-radius: 20rpx;
color: #555;
font-size: 26rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.02);
transition: all 0.15s;
}
.load-more-btn:active {
background: #f5f5f5;
border-color: #2e7d32;
color: #2e7d32;
}