feat(violation):重构违章列表页面并新增组件- 移除原有列表展示逻辑,使用Items组件渲染列表项

- 新增FixedButton组件用于底部固定按钮- 实现无限滚动加载功能替换原有分页
- 调整搜索栏样式和位置,优化用户体验- 根据用户角色过滤数据展示内容- 添加车辆图片字段支持
- 注释掉处理状态选择功能,待后续完善
This commit is contained in:
2025-10-29 14:41:47 +08:00
parent 164cb594fa
commit 52d2d7c773
5 changed files with 250 additions and 385 deletions

View File

@@ -8,6 +8,8 @@ export interface HjmViolation {
id?: number;
// 车辆编号
code?: string;
// 车辆图片
image?: string;
// 标题
title?: string;
// 文章分类ID

View File

@@ -0,0 +1,38 @@
import React from 'react';
import {View} from '@tarojs/components';
import {Button} from '@nutui/nutui-react-taro'
interface FixedButtonProps {
text?: string;
onClick?: () => void;
icon?: React.ReactNode;
disabled?: boolean;
background?: string;
}
function FixedButton({text, onClick, icon, disabled, background}: FixedButtonProps) {
return (
<>
{/* 底部安全区域占位 */}
<View className="h-20 w-full"></View>
<View
className="fixed z-50 bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3 safe-area-bottom">
<Button
type="primary"
style={{
background
}}
size="large"
block
icon={icon}
disabled={disabled}
className="px-6"
onClick={onClick}>
{text || '新增'}
</Button>
</View>
</>
)
}
export default FixedButton;

View File

@@ -0,0 +1,59 @@
import {useEffect} from "react";
import {Image, Space,Button} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {HjmViolation} from "@/api/hjm/hjmViolation/model";
import navTo from "@/utils/common";
interface BestSellersProps {
data: HjmViolation[];
}
const BestSellers = (props: BestSellersProps) => {
const reload = () => {
// 可以在这里添加重新加载逻辑
}
useEffect(() => {
reload()
}, [])
return (
<View className={'px-2 mb-4'}>
<View className={'flex flex-col justify-between items-center rounded-lg px-3'}>
{props.data?.map((item, index) => {
return (
<View key={item.id || index} className={'flex bg-white rounded-lg w-full p-3 mb-3'}
onClick={() => Taro.navigateTo({url: `/hjm/violation/detail?id=${item.code}`})}>
<View className={'flex flex-col'}>
<Image src={item.image && JSON.parse(item.image)[0].url} mode={'scaleToFill'}
radius="10%" width="70" height="70" className={'mb-1'}/>
{item.userId == Taro.getStorageSync('UserId') && (
<Button type={'default'} size="small" onClick={() => navTo(`/hjm/violation/add?id=${item.id}`)}></Button>
)}
</View>
<View className={'mx-3 flex flex-col'}>
<Space direction={'vertical'}>
<View className={'car-no text-lg font-bold'}>{item.title}</View>
<View className={'flex text-xs text-gray-500'}><span
className={'text-gray-700'}>{item.code}</span></View>
<View className={'flex text-xs text-gray-500'}><span
className={'text-gray-700'}>{item.comments}</span></View>
<View className={'flex text-xs text-gray-500'}><span
className={'text-gray-700'}>{item.createTime}</span></View>
</Space>
</View>
</View>
)
})}
{(!props.data || props.data.length === 0) && (
<View className={'flex justify-center items-center py-10'}>
<View className={'text-gray-500 text-sm'}></View>
</View>
)}
</View>
<View style={{height: '170px'}}></View>
</View>
)
}
export default BestSellers

View File

@@ -274,19 +274,19 @@ function Add() {
} style={{padding: '12px 16px'}}>
</Cell>
<Cell title="处理状态" description={'请选择'} extra={
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
height: '24px'
}}>
<span style={{color: '#333', fontSize: '14px'}}>
{statusOptions.find(option => option.value === formData.status)?.text || '请选择'}
</span>
</div>
} style={{padding: '12px 16px'}} onClick={() => setIsPickerVisible(true)}>
</Cell>
{/*<Cell title="处理状态" description={'请选择'} extra={*/}
{/* <div style={{*/}
{/* display: 'flex',*/}
{/* alignItems: 'center',*/}
{/* justifyContent: 'flex-end',*/}
{/* height: '24px'*/}
{/* }}>*/}
{/* <span style={{color: '#333', fontSize: '14px'}}>*/}
{/* {statusOptions.find(option => option.value === formData.status)?.text || '请选择'}*/}
{/* </span>*/}
{/* </div>*/}
{/*} style={{padding: '12px 16px'}} onClick={() => setIsPickerVisible(true)}>*/}
{/*</Cell>*/}
</Cell.Group>
</div>

View File

@@ -1,407 +1,173 @@
import React, {useEffect, useState} from "react";
import {
Loading,
Empty,
Button,
Input,
Tag,
Space,
Pagination
} from '@nutui/nutui-react-taro'
import {useRouter} from '@tarojs/taro'
import {Search, Calendar, Truck, File, AddCircle} from '@nutui/icons-react-taro'
import Taro, {useDidShow} from '@tarojs/taro'
import {useEffect, useState, CSSProperties} from "react";
import {Search} from '@nutui/icons-react-taro'
import {Button, Input, InfiniteLoading} from '@nutui/nutui-react-taro'
import {pageHjmViolation} from "@/api/hjm/hjmViolation";
import {HjmViolation} from "@/api/hjm/hjmViolation/model";
import Taro from '@tarojs/taro'
import Items from "./Items";
import {useRouter} from '@tarojs/taro'
import {getUserInfo} from "@/api/layout";
import navTo from "@/utils/common";
import FixedButton from "@/components/FixedButton";
const InfiniteUlStyle: CSSProperties = {
height: '80vh',
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
}
/**
* 报险记录列表页面
* 文章终极列表
* @constructor
*/
const List: React.FC = () => {
const ViolationList = () => {
const {params} = useRouter();
const [list, setList] = useState<HjmViolation[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [keywords, setKeywords] = useState<string>('')
const [refreshing, setRefreshing] = useState<boolean>(false)
const [page, setPage] = useState<number>(1)
const [limit, setLimit] = useState<number>(10)
const [total, setTotal] = useState<number>(0)
const [needRefresh, setNeedRefresh] = useState<boolean>(true)
const isReloadingRef = React.useRef<boolean>(false) // 使用useRef防止并发请求
const [list, setList] = useState<HjmViolation[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
console.log(refreshing)
// 获取状态显示
const getStatusDisplay = (status?: number) => {
switch (status) {
case 0:
return {text: '未处理', color: '#faad14', bgColor: '#fffbe6'}
case 1:
return {text: '已处理', color: '#52c41a', bgColor: '#f6ffed'}
case 2:
return {text: '已驳回', color: '#ff4d4f', bgColor: '#fff2f0'}
default:
return {text: '未知', color: '#8c8c8c', bgColor: '#f5f5f5'}
}
const onKeywords = (keywords: string) => {
setKeywords(keywords)
}
const reload = async (showLoading = true) => {
// 防止并发请求使用ref而不state避免重渲染
if (isReloadingRef.current) {
console.log('[防重复] 正在加载中,跳过本次请求')
return
const loadList = async (isRefresh = false) => {
if (loading) return;
setLoading(true)
// 搜索条件
const where: any = {
keywords: keywords.trim(),
page,
limit: 10, // 直接使用10不通过state
}
isReloadingRef.current = true
console.log('[开始加载] showLoading:', showLoading, 'page:', page, 'limit:', limit)
// 读取用户信息
const user = await getUserInfo();
// 不要在这里设置limit避免触发useEffect
// setLimit(10) // 删除这行!
try {
if (showLoading) setLoading(true)
setRefreshing(true)
const where: any = {
keywords: keywords.trim(),
page,
limit: 10, // 直接使用10不通过state
}
const roleCode = Taro.getStorageSync('RoleCode');
if (roleCode == 'kuaidi') {
if (Taro.getStorageSync('OrganizationParentId') == 0) {
// @ts-ignore
where.organizationParentId = Taro.getStorageSync('OrganizationId');
} else {
// @ts-ignore
where.organizationId = Taro.getStorageSync('OrganizationId');
}
}
if(params.id){
where.code = params.id;
}
const res = await pageHjmViolation(where)
console.log('[请求成功] 获取到', res?.list?.length, '条数据')
setList(res?.list || [])
setTotal(res?.count || 0)
} catch (error) {
console.error('[请求失败]', error)
Taro.showToast({
title: '获取报险记录失败',
icon: 'error'
})
} finally {
// 判断身份
const roleCode = Taro.getStorageSync('RoleCode');
if(roleCode == 'kuaidiyuan'){
// @ts-ignore
where.driverId = user.userId;
}
if(roleCode == 'zhandian'){
// @ts-ignore
where.organizationId = user.organizationId;
}
if(roleCode == 'kuaidi'){
// @ts-ignore
where.organizationParentId = user.organizationId;
}
if(roleCode == 'Installer'){
// @ts-ignore
where.installerId = user.userId;
}
if(roleCode == 'user'){
setLoading(false)
setRefreshing(false)
isReloadingRef.current = false
console.log('[加载结束]')
return false;
}
if(params.id){
where.code = params.id;
}
// 获取车辆列表
try {
const res = await pageHjmViolation(where);
if (res?.list && res?.list.length > 0) {
if (isRefresh) {
setList(res.list);
} else {
setList(prevList => [...prevList, ...res.list]);
}
setHasMore(res.list.length >= 10); // 如果返回的数据少于10条说明没有更多了
} else {
if (isRefresh) {
setList([]);
}
setHasMore(false);
}
} catch (error) {
console.error('获取车辆列表失败:', error);
if (isRefresh) {
setList([]);
}
setHasMore(false);
} finally {
setLoading(false);
}
}
const onSearch = () => {
reload()
const reload = async () => {
setPage(1);
await loadList(true);
}
const onKeywordsChange = (value: string) => {
setKeywords(value)
const loadMore = async () => {
if (!hasMore || loading) return;
const nextPage = page + 1;
setPage(nextPage);
await loadList();
}
const onAddInsurance = () => {
Taro.navigateTo({
url: '/hjm/violation/add'
})
}
// 页面显示时触发,包括从其他页面返回
useDidShow(() => {
console.log('[生命周期] useDidShow 触发, needRefresh:', needRefresh)
if (needRefresh) {
console.log('[生命周期] 执行reload 由于 needRefresh=true')
// 先设置false再调用reload防止多次触发
setNeedRefresh(false)
reload(false)
}
})
// 初始化effect - 只执行一次
useEffect(() => {
console.log('[生命周期] 组件初始化')
// 监听刷新事件
const handleRefresh = () => {
console.log('[事件] 接收到 violationListRefresh 事件')
setNeedRefresh(true)
}
Taro.eventCenter.on('violationListRefresh', handleRefresh)
// 初始加载数据
console.log('[生命周期] 执行初始加载')
reload().then()
// 清理事件监听器
return () => {
console.log('[生命周期] 组件卸载,清理事件监听')
Taro.eventCenter.off('violationListRefresh', handleRefresh)
}
}, []) // 空依赖,只执行一次
// 分页变化effect - 单独处理
useEffect(() => {
// 跳过初始值,只在真正变化时才加载
if (page !== 1) {
console.log('[分页变化] page变为', page, '执行reload')
reload()
}
}, [page])
useEffect(() => {
// limit变化时重置到第一页
if (limit !== 10) {
console.log('[分页变化] limit变为', limit, ',重置到第一页')
setPage(1)
reload()
}
}, [limit])
const onPageChange = (current: number) => {
setPage(current)
}
}, [])
return (
<>
{/* 搜索栏 */}
<div style={{
position: 'fixed',
top: '20px',
left: 0,
right: 0,
display: "none",
zIndex: 20,
padding: '0 16px',
backgroundColor: '#f5f5f5'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
backgroundColor: '#fff',
padding: '8px 12px',
borderRadius: '20px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}>
<Search size={16} color="#999"/>
<Input
placeholder="搜索报险记录"
value={keywords}
onChange={onKeywordsChange}
onConfirm={onSearch}
style={{
border: 'none',
backgroundColor: 'transparent',
flex: 1,
marginLeft: '8px'
}}
/>
<Button
type="primary"
size="small"
onClick={onSearch}
loading={loading}
>
</Button>
</div>
</div>
{/* 报险记录列表 */}
<div style={{
marginTop: '10px',
paddingBottom: '80px'
}}>
{loading && list.length === 0 ? (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '200px'
}}>
<Loading type="spinner">...</Loading>
</div>
) : list.length === 0 ? (
<Empty description="暂无违章记录">
</Empty>
) : (
<div style={{padding: '0 16px'}}>
{list.map((item) => {
const statusDisplay = getStatusDisplay(item.status)
return (
<div
key={item.id || item.code || `item-${item.title}-${item.createTime}`} // 使用唯一key
style={{
backgroundColor: '#fff',
borderRadius: '12px',
padding: '16px',
marginBottom: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
border: '1px solid #f0f0f0'
}}
onClick={() => {
Taro.navigateTo({
url: `/hjm/violation/detail?id=${item.code}`
})
}}
>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '12px'
}}>
<div style={{flex: 1}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '8px'
}}>
<File size={16} color="#1890ff"/>
<span style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#262626'
}}>
{item.title}
</span>
</div>
<Space direction="vertical">
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<Truck size={14} color="#8c8c8c"/>
<span style={{fontSize: '13px', color: '#8c8c8c'}}>
{item.code}
</span>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<Calendar size={14} color="#8c8c8c"/>
<span style={{fontSize: '13px', color: '#8c8c8c'}}>
{item.createTime}
</span>
</div>
</Space>
</div>
<Tag
color={statusDisplay.color}
style={{
backgroundColor: statusDisplay.bgColor,
border: `1px solid ${statusDisplay.color}`,
fontSize: '12px'
}}
>
{statusDisplay.text}
</Tag>
</div>
{/* 备注信息 */}
{item.comments && (
<div style={{
backgroundColor: '#f8f9fa',
padding: '8px 12px',
borderRadius: '6px',
fontSize: '13px',
color: '#595959',
lineHeight: '1.4'
}}>
{item.comments.length > 50
? `${item.comments.substring(0, 50)}...`
: item.comments
}
</div>
)}
{item.userId == Taro.getStorageSync('UserId') && (
<div className={'flex justify-end mt-4'}>
<Space>
<Button type="default" onClick={(event: any) => {
event.stopPropagation()
Taro.navigateTo({
url: `/hjm/violation/add?id=${item.id}`
})
}}></Button>
{/*<Button type="primary" onClick={(event: any) => {*/}
{/* event.stopPropagation()*/}
{/* removeHjmViolation(item.id).then(() => {*/}
{/* Taro.showToast({*/}
{/* title: '删除成功',*/}
{/* icon: 'success'*/}
{/* })*/}
{/* // 删除成功后重新加载列表*/}
{/* reload(false)*/}
{/* }).catch((error) => {*/}
{/* Taro.showToast({*/}
{/* title: error.message || '删除失败',*/}
{/* icon: 'error'*/}
{/* })*/}
{/* })*/}
{/*}}>删除</Button>*/}
</Space>
</div>
)}
</div>
)
})}
</div>
)}
</div>
{
Taro.getStorageSync('RoleCode') == 'jiaojing' && (
<div className={'fixed z-20 top-5 left-0 w-full'}>
<div className={'px-4'}>
<div
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: 30,
padding: '8px',
borderRadius: '20px',
overflow: "hidden",
backgroundColor: '#ff0000',
}}>
<AddCircle size={28} color={'#ffffff'} onClick={onAddInsurance}/>
display: 'flex',
alignItems: 'center',
background: '#fff',
padding: '0 10px',
borderRadius: '20px'
}}
>
<Search/>
<Input
placeholder="车辆编号"
value={keywords}
onChange={onKeywords}
onConfirm={reload}
/>
<div
className={'flex items-center'}
>
<Button type="warning" onClick={reload}>
</Button>
</div>
</div>
</div>
</div>
<div style={InfiniteUlStyle} id="scroll">
<InfiniteLoading
target="scroll"
className={'w-full fixed left-0 top-20'}
hasMore={hasMore}
onLoadMore={loadMore}
loadingText="加载中..."
loadMoreText="没有更多了"
>
<Items data={list}/>
</InfiniteLoading>
</div>
{
Taro.getStorageSync('RoleCode') == 'jiaojing' && (
<>
<FixedButton onClick={() => navTo(`/hjm/violation/add`)} />
</>
)
}
{/* 分页 */}
{list.length > 0 && (
<div style={{
display: 'flex',
justifyContent: 'center',
padding: '20px 0',
backgroundColor: '#f5f5f5'
}}>
<Pagination
value={page}
total={total}
pageSize={limit}
onChange={onPageChange}
/>
</div>
)}
</>
)
}
export default List
export default ViolationList