完成:黄家明项目的开发并存档

This commit is contained in:
2025-06-20 19:55:23 +08:00
parent f83b856438
commit 1b143c0c1f
18 changed files with 1295 additions and 84 deletions

View File

@@ -52,6 +52,8 @@ export interface HjmCar {
address?: string,
// 用户ID
userId?: number;
// 认领状态
claim?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { HjmViolation, HjmViolationParam } from './model';
/**
* 分页查询黄家明_违章记录
*/
export async function pageHjmViolation(params: HjmViolationParam) {
const res = await request.get<ApiResult<PageResult<HjmViolation>>>(
'/hjm/hjm-violation/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询黄家明_违章记录列表
*/
export async function listHjmViolation(params?: HjmViolationParam) {
const res = await request.get<ApiResult<HjmViolation[]>>(
'/hjm/hjm-violation',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加黄家明_违章记录
*/
export async function addHjmViolation(data: HjmViolation) {
const res = await request.post<ApiResult<unknown>>(
'/hjm/hjm-violation',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改黄家明_违章记录
*/
export async function updateHjmViolation(data: HjmViolation) {
const res = await request.put<ApiResult<unknown>>(
'/hjm/hjm-violation',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除黄家明_违章记录
*/
export async function removeHjmViolation(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/hjm/hjm-violation/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除黄家明_违章记录
*/
export async function removeBatchHjmViolation(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/hjm/hjm-violation/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询黄家明_违章记录
*/
export async function getHjmViolation(id: number) {
const res = await request.get<ApiResult<HjmViolation>>(
'/hjm/hjm-violation/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,43 @@
import type { PageParam } from '@/api/index';
/**
* 黄家明_违章记录
*/
export interface HjmViolation {
// 自增ID
id?: number;
// 车辆编号
code?: string;
// 标题
title?: string;
// 文章分类ID
categoryId?: number;
// 处罚金额
money?: string;
// 扣分
score?: string;
// 录入员
adminId?: number;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0未处理, 1已处理
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 黄家明_违章记录搜索条件
*/
export interface HjmViolationParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -15,7 +15,8 @@ export default defineAppConfig({
"register",
"forget",
"setting",
"agreement"
"agreement",
"sms-login"
]
},
{
@@ -34,7 +35,8 @@ export default defineAppConfig({
"company/company",
"profile/profile",
"setting/setting",
"userVerify/index"
"userVerify/index",
"userVerify/admin"
]
},
{
@@ -48,6 +50,8 @@ export default defineAppConfig({
"exam/exam",
"bx/bx",
"bx/bx-add",
"violation/add",
"violation/list",
"trajectory/trajectory",
"gps-log/gps-log"
// "bx/bx-list",

View File

@@ -33,8 +33,13 @@ const List = () => {
where.organizationId = Taro.getStorageSync('OrganizationId');
}
if(roleCode == 'kuaidi'){
if(Taro.getStorageSync('OrganizationId') == Taro.getStorageSync('OrganizationParentId')){
// @ts-ignore
where.organizationParentId = Taro.getStorageSync('OrganizationParentId');
}else {
// @ts-ignore
where.organizationId = Taro.getStorageSync('OrganizationId');
}
}
// 获取车辆列表

View File

@@ -115,7 +115,7 @@ const Query = () => {
title: '请上传车辆图片',
icon: 'error'
});
// return false
return false
}
if (!FormData.gpsNo) {
Taro.showToast({
@@ -130,7 +130,7 @@ const Query = () => {
...FormData,
status: 1
}).then(() => {
Taro.showToast({title: `绑定成功`, icon: 'success'})
Taro.showToast({title: `安装成功`, icon: 'success'})
setTimeout(() => {
return Taro.navigateBack()
}, 1000)
@@ -311,7 +311,7 @@ const Query = () => {
const onClaimVehicle = () => {
updateHjmCar({
...FormData,
status: 2,
claim: 1,
driverId: Taro.getStorageSync('UserId'),
driverName: Taro.getStorageSync('RealName')
}).then(() => {
@@ -361,6 +361,7 @@ const Query = () => {
return false
}
// 4.查询角色
const role = await listUserRole({userId: Taro.getStorageSync('UserId')})
const roleCode = role[0].roleCode;
@@ -394,9 +395,15 @@ const Query = () => {
}
}
}
// 1.符合条件则由安装人员安装车辆,否则提示无权限
console.log(roleCode,'roleCode..')
// 1.已认领则展示车辆
if (carInfo.status == 2) {
setClaimVehicle(false)
Taro.setNavigationBarTitle({
title: '车辆信息'
})
return false
}
// 2.符合条件则由安装人员安装车辆,否则提示无权限
if (carInfo.status == 0 && roleCode != 'Installer') {
Taro.setNavigationBarTitle({
title: '安装设备'
@@ -408,24 +415,29 @@ const Query = () => {
setDisabled(true)
return false
}
// 2.如果已安装,则判断是否已认领车辆
// 3.如果已安装,则判断是否已认领车辆
if (carInfo.status == 1 && roleCode == 'kuaidiyuan') {
// 2.1先查询名下有多少辆车
const carCount = await pageHjmCar({driverId: Taro.getStorageSync('UserId')})
if (carCount?.count && carCount?.count == 0) {
if (carCount?.count == 0) {
// 2.2无车辆则认领
setClaimVehicle(true)
Taro.setNavigationBarTitle({
title: '认领车辆'
})
} else {
// 2.3存在车辆则展示车辆信息
setClaimVehicle(false)
Taro.setNavigationBarTitle({
title: '车辆信息'
})
}
return false
}
// if(carInfo.status == 1 && Taro.getStorageSync('UserId') != carInfo.driverId){
// Taro.showToast({
// title: '暂无权限',
// icon: 'error'
// })
// setTimeout(() => {
// Taro.navigateBack()
// },2000)
// return false
// }
}
}
@@ -448,6 +460,8 @@ const Query = () => {
})
}, [])
// @ts-ignore
// @ts-ignore
return (
<>
{/* 未安装 */}
@@ -539,10 +553,11 @@ const Query = () => {
onChange={(value) => setFormData({...FormData, gpsNo: value})}
/>
<div
className="right"
className="right p-1"
style={{display: 'flex', alignItems: 'center'}}
onClick={saveGpsNo}
>
<Scan onClick={saveGpsNo}/>
<Scan />
</div>
</div>
</Form.Item>
@@ -637,7 +652,6 @@ const Query = () => {
)}
</div>
</Form.Item>
<Form.Item
label={'操作员'}
name="driver"
@@ -648,10 +662,7 @@ const Query = () => {
</Form>
</div>
</div>
) : ''}
{/* 已安装 */}
{FormData?.status != 0 ? (
) : (
<div className={'car-info w-full bg-white'}>
{/* 显示多张图片 */}
<div style={{
@@ -705,6 +716,9 @@ const Query = () => {
<Cell className={'car-info-item-content'}>
{FormData.fenceName}
</Cell>
<Cell className={'car-info-item-content'}>
{FormData.status == 2 ? '已绑定' : '未绑定'}
</Cell>
<Cell className={'car-info-item-content'}>
{FormData.status == 2 ? FormData.driver : '-'}
</Cell>
@@ -754,7 +768,7 @@ const Query = () => {
}
</div>
</div>
) : ''}
)}
</>
)
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '添加违章记录'
})

244
src/hjm/violation/add.tsx Normal file
View File

@@ -0,0 +1,244 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {
Button,
TextArea,
Cell,
Input,
} from '@nutui/nutui-react-taro'
import {addHjmViolation} from "@/api/hjm/hjmViolation";
import {HjmViolation} from "@/api/hjm/hjmViolation/model";
/**
* 添加违章记录页面
*/
function Add() {
const [loading, setLoading] = useState<boolean>(false)
const [lastSubmitTime, setLastSubmitTime] = useState<number>(0) // 最后提交时间
const [formData, setFormData] = useState<HjmViolation>({
code: '',
title: '',
money: '',
score: '',
comments: ''
})
// 初始化页面数据
const initPageData = async () => {
try {
setLoading(false)
} catch (error) {
console.error('初始化失败:', error)
Taro.showToast({
title: '初始化失败',
icon: 'none'
})
}
}
// 提交表单
const handleSubmit = async () => {
// 防止重复提交 - 检查loading状态
if (loading) {
Taro.showToast({
title: '正在提交中,请稍候...',
icon: 'loading'
})
return
}
// 防止快速连续点击 - 2秒内不允许重复提交
const now = Date.now()
if (now - lastSubmitTime < 2000) {
Taro.showToast({
title: '请勿频繁提交',
icon: 'none'
})
return
}
setLastSubmitTime(now)
// 表单验证
if (!formData.code?.trim()) {
Taro.showToast({
title: '请输入车辆编号',
icon: 'none'
})
return
}
if (!formData.title?.trim()) {
Taro.showToast({
title: '请输入违章标题',
icon: 'none'
})
return
}
// if (!formData.money?.trim()) {
// Taro.showToast({
// title: '请输入处罚金额',
// icon: 'none'
// })
// return
// }
//
// if (!formData.score?.trim()) {
// Taro.showToast({
// title: '请输入扣分',
// icon: 'none'
// })
// return
// }
setLoading(true)
// 构建提交数据
const submitData: HjmViolation = {
...formData,
status: 0 // 0未处理, 1已处理
}
addHjmViolation(submitData).then((res) => {
console.log(res)
Taro.showToast({
title: '提交成功',
icon: 'success'
})
// 清空表单
setFormData({
code: '',
title: '',
money: '',
score: '',
comments: ''
})
setTimeout(() => {
Taro.navigateBack()
}, 2000)
}).finally(() => {
setLoading(false)
})
}
useEffect(() => {
initPageData().then(r => {
console.log(r, 'rr')
})
}, [])
return (
<div style={{
backgroundColor: '#f5f5f5',
minHeight: '100vh',
paddingBottom: '80px'
}}>
{/* 页面标题 */}
{/*<div style={{*/}
{/* backgroundColor: '#fff',*/}
{/* margin: '16px',*/}
{/* borderRadius: '12px',*/}
{/* padding: '16px',*/}
{/* boxShadow: '0 2px 8px rgba(0,0,0,0.06)'*/}
{/*}}>*/}
{/* <div style={{*/}
{/* display: 'flex',*/}
{/* alignItems: 'center',*/}
{/* gap: '8px',*/}
{/* marginBottom: '12px'*/}
{/* }}>*/}
{/* <Truck size={18} color="#1890ff"/>*/}
{/* <span style={{fontSize: '16px', fontWeight: 'bold'}}>添加违章记录</span>*/}
{/* </div>*/}
{/*</div>*/}
{/* 违章信息表单 */}
<div style={{
backgroundColor: '#fff',
margin: '16px 16px 16px',
borderRadius: '12px',
overflow: 'hidden'
}}>
<div style={{
padding: '16px',
borderBottom: '1px solid #f0f0f0'
}}>
<span style={{fontSize: '16px', fontWeight: 'bold'}}></span>
</div>
<Cell.Group>
<Cell title="车辆编号" style={{padding: '12px 16px'}}>
<Input
placeholder="YT000001"
value={formData.code}
onChange={(value) => setFormData({...formData, code: value})}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
</Cell>
<Cell title="违章标题" style={{padding: '12px 16px'}}>
<Input
placeholder="不按规定停车"
value={formData.title}
onChange={(value) => setFormData({...formData, title: value})}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
</Cell>
<Cell title="违章描述" style={{padding: '12px 16px'}}>
<TextArea
placeholder={'XX路1号...'}
value={formData.comments}
onChange={(value) => setFormData({...formData, comments: value})}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
</Cell>
<Cell title="处罚金额" style={{padding: '12px 16px'}}>
<Input
type="number"
placeholder="200(元)"
maxLength={3}
value={formData.money}
onChange={(value) => setFormData({...formData, money: value})}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
</Cell>
<Cell title="扣分" style={{padding: '12px 16px'}}>
<Input
type="number"
maxLength={2}
placeholder="2(分)"
value={formData.score}
onChange={(value) => setFormData({...formData, score: value})}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
</Cell>
</Cell.Group>
</div>
{/* 提交按钮 */}
<div style={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#fff',
padding: '16px',
borderTop: '1px solid #f0f0f0'
}}>
<Button
type="primary"
block
loading={loading}
disabled={loading}
onClick={handleSubmit}
>
{loading ? '提交中...' : '提交违章记录'}
</Button>
</div>
</div>
)
}
export default Add

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '违章记录'
})

260
src/hjm/violation/list.tsx Normal file
View File

@@ -0,0 +1,260 @@
import React, {useEffect, useState} from "react";
import {
Loading,
Empty,
Button,
Input,
Tag,
Space
} from '@nutui/nutui-react-taro'
import {Search, Calendar, Truck, File, AddCircle} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {pageHjmViolation} from "@/api/hjm/hjmViolation";
import {HjmViolation} from "@/api/hjm/hjmViolation/model";
/**
* 报险记录列表页面
*/
const List: React.FC = () => {
const [list, setList] = useState<HjmViolation[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [keywords, setKeywords] = useState<string>('')
const [refreshing, setRefreshing] = useState<boolean>(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 reload = async (showLoading = true) => {
try {
if (showLoading) setLoading(true)
setRefreshing(true)
const res = await pageHjmViolation({
keywords: keywords.trim(),
})
setList(res?.list || [])
} catch (error) {
console.error('获取报险记录失败:', error)
Taro.showToast({
title: '获取报险记录失败',
icon: 'error'
})
} finally {
setLoading(false)
setRefreshing(false)
}
}
const onSearch = () => {
reload()
}
const onKeywordsChange = (value: string) => {
setKeywords(value)
}
const onAddInsurance = () => {
Taro.navigateTo({
url: '/hjm/violation/add'
})
}
useEffect(() => {
reload().then()
}, [])
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="暂无报险记录">
<Button type="primary" onClick={onAddInsurance}>
</Button>
</Empty>
) : (
<div style={{padding: '0 16px'}}>
{list.map((item, index) => {
const statusDisplay = getStatusDisplay(item.status)
return (
<div
key={index}
style={{
backgroundColor: '#fff',
borderRadius: '12px',
padding: '16px',
marginBottom: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
border: '1px solid #f0f0f0'
}}
>
<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>
)}
</div>
)
})}
</div>
)}
</div>
<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} />
</div>
</>
)
}
export default List

View File

@@ -39,6 +39,12 @@ const ExpirationTime = () => {
if(data.certification){
setIsAdmin( true)
}
if(Taro.getStorageSync('certification') == 'jj'){
setIsAdmin(true)
}
if(Taro.getStorageSync('certification') == 'yz'){
setIsAdmin(true)
}
data.roles?.map((item, index) => {
if (index == 0) {
setRoleName(item.roleCode)

View File

@@ -65,6 +65,7 @@ const Header = (props: any) => {
setRoleName('交警')
Taro.setStorageSync('RoleName', '交警')
Taro.setStorageSync('RoleCode', 'jiaojing')
Taro.setStorageSync('certification', 'jj')
return false;
}
// 邮政协会/管局
@@ -73,6 +74,7 @@ const Header = (props: any) => {
setRoleName('邮政协会/管局')
Taro.setStorageSync('RoleName', '邮政协会/管局')
Taro.setStorageSync('RoleCode', 'youzheng')
Taro.setStorageSync('certification', 'yz')
return false;
}
// 快递公司

View File

@@ -116,9 +116,8 @@ function Home() {
setLongitude(res.longitude)
}
console.log(Taro.getStorageSync('RoleName'))
// 已认证用户
if(Taro.getStorageSync('Certification')){
if (Taro.getStorageSync('certification')) {
setIsAdmin(true)
setScale(11)
pageHjmCarByMap(res.latitude, res.longitude)
@@ -308,10 +307,14 @@ function Home() {
getUserInfo().then((data) => {
if (data) {
// 是否管理员
console.log(data.certification, 'certification')
if (data.certification) {
setIsAdmin(true)
}
// 是否交警
if (Taro.getStorageSync('certification') == 'jj') {
console.log('交警', '12312')
setIsAdmin(true)
}
setUserInfo(data)
setIsLogin(true);
Taro.setStorageSync('UserId', data.userId)
@@ -319,9 +322,12 @@ function Home() {
if (!data.openid) {
Taro.login({
success: (res) => {
// 排查交警和邮政角色不保存openid
if (Taro.getStorageSync('RoleCode') !== 'jiaojing' || Taro.getStorageSync('RoleCode') !== 'youzheng' || Taro.getStorageSync('RoleCode') !== 'Installer') {
getWxOpenId({code: res.code}).then(() => {
})
}
}
})
}
}

View File

@@ -3,9 +3,10 @@ import navTo from "@/utils/common";
import UserFooter from "./UserFooter";
import Taro from '@tarojs/taro'
import {ArrowRight, ShieldCheck, Truck, LogisticsError} from '@nutui/icons-react-taro'
import {CSSProperties} from "react";
import {CSSProperties, useEffect, useState} from "react";
const UserCell = () => {
const [roleName, setRoleName] = useState<string>('')
const InfiniteUlStyle: CSSProperties = {
height: '88vh',
padding: '16px',
@@ -30,6 +31,10 @@ const UserCell = () => {
})
}
useEffect(() => {
setRoleName(Taro.getStorageSync('RoleCode'))
}, []);
return (
<>
<div style={InfiniteUlStyle} id="scroll">
@@ -50,6 +55,29 @@ const UserCell = () => {
}}
/>
</Cell.Group>
{
roleName === 'kuaidi' && (
<Cell.Group divider={true}>
<Cell
className="nutui-cell-clickable"
title={
<div style={{display: 'inline-flex', alignItems: 'center'}}>
<ShieldCheck size={16}/>
<span className={'pl-3 text-sm'}></span>
</div>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/userVerify/admin', true)
}}
/>
</Cell.Group>
)
}
{
roleName === 'kuaidiyuan' && (
<>
<Cell.Group divider={true}>
<Cell
className="nutui-cell-clickable"
@@ -82,6 +110,30 @@ const UserCell = () => {
}}
/>
</Cell.Group>
</>
)
}
{
roleName === 'jiaojing' && (
<Cell.Group divider={true}>
<Cell
className="nutui-cell-clickable"
title={
<div style={{display: 'inline-flex', alignItems: 'center'}}>
<Truck size={16}/>
<span className={'pl-3 text-sm'}></span>
</div>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/hjm/violation/list', true)
}}
/>
</Cell.Group>
)
}
{/*<Cell.Group divider={true} description={*/}
{/* <div style={{display: 'inline-flex', alignItems: 'center'}}>*/}
{/* <span style={{marginTop: '12px'}}>管理</span>*/}
@@ -173,6 +225,13 @@ const UserCell = () => {
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/user/profile/profile', true)}
/>
<Cell
className="nutui-cell-clickable"
title="短信登录"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/passport/sms-login', true)}
/>
<Cell
className="nutui-cell-clickable"
title="退出登录"

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '验证码登录',
navigationBarTextStyle: 'black'
})

204
src/passport/sms-login.tsx Normal file
View File

@@ -0,0 +1,204 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {Input, Button} from '@nutui/nutui-react-taro'
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
import {LoginParam} from "@/api/passport/login/model";
const SmsLogin = () => {
const [loading, setLoading] = useState<boolean>(false)
const [sendingCode, setSendingCode] = useState<boolean>(false)
const [countdown, setCountdown] = useState<number>(0)
const [formData, setFormData] = useState<LoginParam>({
phone: '',
code: ''
})
const reload = () => {
Taro.hideTabBar()
}
useEffect(() => {
reload()
}, [])
// 倒计时效果
useEffect(() => {
let timer: NodeJS.Timeout
if (countdown > 0) {
timer = setTimeout(() => {
setCountdown(countdown - 1)
}, 1000)
}
return () => {
if (timer) clearTimeout(timer)
}
}, [countdown])
// 验证手机号格式
const validatePhone = (phone: string): boolean => {
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(phone)
}
// 发送短信验证码
const handleSendCode = async () => {
if (!formData.phone) {
Taro.showToast({
title: '请输入手机号码',
icon: 'none'
})
return
}
if (!validatePhone(formData.phone)) {
Taro.showToast({
title: '请输入正确的手机号码',
icon: 'none'
})
return
}
if (sendingCode || countdown > 0) {
return
}
try {
setSendingCode(true)
await sendSmsCaptcha({ phone: formData.phone })
Taro.showToast({
title: '验证码已发送',
icon: 'success'
})
// 开始60秒倒计时
setCountdown(60)
} catch (error: any) {
Taro.showToast({
title: error.message || '发送失败',
icon: 'error'
})
} finally {
setSendingCode(false)
}
}
// 处理登录
const handleLogin = async () => {
// 防止重复提交
if (loading) {
return
}
// 表单验证
if (!formData.phone) {
Taro.showToast({
title: '请输入手机号码',
icon: 'none'
})
return
}
if (!validatePhone(formData.phone)) {
Taro.showToast({
title: '请输入正确的手机号码',
icon: 'none'
})
return
}
if (!formData.code) {
Taro.showToast({
title: '请输入验证码',
icon: 'none'
})
return
}
if (formData.code.length !== 6) {
Taro.showToast({
title: '请输入6位验证码',
icon: 'none'
})
return
}
try {
setLoading(true)
await loginBySms({
phone: formData.phone,
code: formData.code
})
Taro.showToast({
title: '登录成功',
icon: 'success'
})
// 延迟跳转到首页
setTimeout(() => {
Taro.reLaunch({
url: '/pages/index/index'
})
}, 1500)
} catch (error: any) {
Taro.showToast({
title: error.message || '登录失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}
return (
<>
<div className={'flex flex-col justify-center px-5 pt-3'}>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input
type="number"
placeholder="请输入手机号码"
maxLength={11}
value={formData.phone}
onChange={(value) => setFormData({...formData, phone: value})}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
</div>
<div className={'flex justify-between items-center bg-white rounded-lg my-2 pr-2'}>
<Input
type="number"
placeholder="请输入6位验证码"
maxLength={6}
value={formData.code}
onChange={(value) => setFormData({...formData, code: value})}
style={{ backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
<Button
size="small"
type={countdown > 0 ? "default" : "primary"}
loading={sendingCode}
disabled={sendingCode || countdown > 0}
onClick={handleSendCode}
>
{countdown > 0 ? `${countdown}s` : sendingCode ? '发送中...' : '获取验证码'}
</Button>
</div>
<div className={'flex justify-center my-5'}>
<Button
type="info"
size={'large'}
className={'w-full rounded-lg p-2'}
loading={loading}
disabled={loading}
onClick={handleLogin}
>
{loading ? '登录中...' : '登录'}
</Button>
</div>
</div>
</>
)
}
export default SmsLogin

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '实名认证管理'
})

View File

@@ -0,0 +1,248 @@
import React, {useEffect, useState} from "react";
import {
Loading,
Empty,
Button,
Input,
Tag,
Space
} from '@nutui/nutui-react-taro'
import {Search, Calendar, Truck, File, AddCircle} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {pageHjmViolation} from "@/api/hjm/hjmViolation";
import {UserVerify} from "@/api/system/userVerify/model";
import {pageUserVerify} from "@/api/system/userVerify";
/**
* 报险记录列表页面
*/
const List: React.FC = () => {
const [list, setList] = useState<UserVerify[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [keywords, setKeywords] = useState<string>('')
const [refreshing, setRefreshing] = useState<boolean>(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 reload = async (showLoading = true) => {
try {
if (showLoading) setLoading(true)
setRefreshing(true)
const res = await pageUserVerify({
keywords: keywords.trim(),
})
setList(res?.list || [])
} catch (error) {
console.error('获取报险记录失败:', error)
Taro.showToast({
title: '获取报险记录失败',
icon: 'error'
})
} finally {
setLoading(false)
setRefreshing(false)
}
}
const onSearch = () => {
reload()
}
const onKeywordsChange = (value: string) => {
setKeywords(value)
}
const onAddInsurance = () => {
Taro.navigateTo({
url: '/hjm/violation/add'
})
}
useEffect(() => {
reload().then()
}, [])
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="暂无报险记录">
<Button type="primary" onClick={onAddInsurance}>
</Button>
</Empty>
) : (
<div style={{padding: '0 16px'}}>
{list.map((item, index) => {
const statusDisplay = getStatusDisplay(item.status)
return (
<div
key={index}
style={{
backgroundColor: '#fff',
borderRadius: '12px',
padding: '16px',
marginBottom: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
border: '1px solid #f0f0f0'
}}
>
<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.realName}
</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.phone}
</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'
}}
>
{item.statusText}
</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>
)}
</div>
)
})}
</div>
)}
</div>
</>
)
}
export default List