feat(credit): 重构小程序端客户管理功能

- 优化代码结构,使用函数组件和Hooks替代类组件
- 实现表单验证和错误处理机制
- 添加加载状态和提交防抖保护
- 增加城市选择和文件上传功能
- 实现推荐状态切换和长按删除功能
- 统一错误提示和用户反馈机制
- 优化页面标题和导航体验
This commit is contained in:
2026-03-16 22:12:16 +08:00
parent 694efb77ec
commit 138f28793f
4 changed files with 314 additions and 168 deletions

View File

@@ -1,98 +1,136 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Button, Loading, CellGroup, Input, TextArea, Form} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {CreditMpCustomer} from "@/api/credit/creditMpCustomer/model";
import {getCreditMpCustomer, listCreditMpCustomer, updateCreditMpCustomer, addCreditMpCustomer} from "@/api/credit/creditMpCustomer";
import { useEffect, useMemo, useRef, useState } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { Button, CellGroup, Form, Input, Loading, TextArea } from '@nutui/nutui-react-taro'
import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model'
import { addCreditMpCustomer, getCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
export default function CreditMpCustomerAddPage() {
const { params } = useRouter()
const id = useMemo(() => {
const n = Number(params?.id)
return Number.isFinite(n) && n > 0 ? n : undefined
}, [params?.id])
const AddCreditMpCustomer = () => {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<CreditMpCustomer>({})
const formRef = useRef<any>(null)
const reload = async () => {
if (params.id) {
const data = await getCreditMpCustomer(Number(params.id))
setFormData(data)
} else {
setFormData({})
}
}
// 提交表单
const submitSucceed = async (values: any) => {
try {
if (params.id) {
// 编辑模式
await updateCreditMpCustomer({
...values,
id: Number(params.id)
})
} else {
// 新增模式
await addCreditMpCustomer(values)
}
Taro.showToast({
title: `操作成功`,
icon: 'success'
})
setTimeout(() => {
return Taro.navigateBack()
}, 1000)
} catch (error) {
Taro.showToast({
title: `操作失败`,
icon: 'error'
});
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
const [loading, setLoading] = useState(true)
const [initialValues, setInitialValues] = useState<Partial<CreditMpCustomer>>({})
const [submitting, setSubmitting] = useState(false)
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, []);
Taro.setNavigationBarTitle({ title: id ? '编辑小程序端客户' : '新增小程序端客户' })
}, [id])
useEffect(() => {
const run = async () => {
setLoading(true)
try {
if (id) {
const data = await getCreditMpCustomer(id)
setInitialValues(data || {})
} else {
setInitialValues({})
}
} catch (e) {
console.error('加载失败:', e)
Taro.showToast({ title: (e as any)?.message || '加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}
run()
}, [id])
const onFinish = async (values: any) => {
if (submitting) return
setSubmitting(true)
try {
if (id) {
await updateCreditMpCustomer({ ...values, id })
} else {
await addCreditMpCustomer(values)
}
Taro.showToast({ title: '操作成功', icon: 'success' })
setTimeout(() => Taro.navigateBack(), 800)
} catch (e) {
console.error('保存失败:', e)
Taro.showToast({ title: (e as any)?.message || '操作失败', icon: 'none' })
} finally {
setSubmitting(false)
}
}
if (loading) {
return <Loading className={'px-2'}></Loading>
return <Loading className="px-2"></Loading>
}
return (
<>
<View className="bg-gray-50 min-h-screen px-3 pt-3 pb-6">
<Form
ref={formRef}
divider
initialValues={FormData}
initialValues={initialValues}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
onFinish={onFinish}
footer={
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Button
nativeType="submit"
type="success"
size="large"
className={'w-full'}
block
>
{params.id ? '更新' : '保存'}
</Button>
</div>
<Button nativeType="submit" type="primary" size="large" loading={submitting} block>
{id ? '更新' : '保存'}
</Button>
}
>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="toUser" label="拖欠方" initialValue={FormData.toUser} required>
<CellGroup>
<Form.Item
name="toUser"
label="拖欠方"
required
rules={[{ required: true, message: '请输入拖欠方' }]}
>
<Input placeholder="请输入拖欠方/付款方名称" maxLength={50} />
</Form.Item>
<Form.Item
name="price"
label="拖欠金额"
required
rules={[{ required: true, message: '请输入拖欠金额' }]}
>
<Input placeholder="请输入拖欠金额" type="number" />
</Form.Item>
<Form.Item
name="years"
label="拖欠年数"
required
rules={[{ required: true, message: '请输入拖欠年数' }]}
>
<Input placeholder="请输入拖欠年数" type="number" />
</Form.Item>
<Form.Item name="province" label="省份">
<Input placeholder="可选" maxLength={20} />
</Form.Item>
<Form.Item name="city" label="城市">
<Input placeholder="可选" maxLength={20} />
</Form.Item>
<Form.Item name="region" label="辖区">
<Input placeholder="可选" maxLength={20} />
</Form.Item>
<Form.Item name="url" label="链接">
<Input placeholder="可选" maxLength={200} />
</Form.Item>
<Form.Item name="files" label="文件">
<TextArea placeholder="可选可填文件URL或JSON" maxLength={1000} rows={3} />
</Form.Item>
<Form.Item name="comments" label="备注">
<TextArea placeholder="可选" maxLength={200} showCount rows={3} />
</Form.Item>
</CellGroup>
</Form>
</View>
)
}

View File

@@ -1,64 +1,129 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {CreditMpCustomer} from "@/api/credit/creditMpCustomer/model";
import {listCreditMpCustomer, removeCreditMpCustomer, updateCreditMpCustomer} from "@/api/credit/creditMpCustomer";
import { useCallback, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Button, Cell, CellGroup, ConfigProvider, Empty, Space } from '@nutui/nutui-react-taro'
import { ArrowRight, CheckNormal, Checked } from '@nutui/icons-react-taro'
const CreditMpCustomerList = () => {
import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model'
import { listCreditMpCustomer, removeCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
export default function CreditMpCustomerListPage() {
const [list, setList] = useState<CreditMpCustomer[]>([])
const [loading, setLoading] = useState(false)
const reload = () => {
listCreditMpCustomer({
// 添加查询条件
})
.then(data => {
setList(data || [])
})
.catch(() => {
Taro.showToast({
title: '获取数据失败',
icon: 'error'
});
})
}
const onDel = async (id?: number) => {
await removeCreditMpCustomer(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload();
}
const reload = useCallback(async () => {
setLoading(true)
try {
const data = await listCreditMpCustomer()
setList(data || [])
} catch (e) {
console.error('获取数据失败:', e)
Taro.showToast({ title: (e as any)?.message || '获取数据失败', icon: 'none' })
} finally {
setLoading(false)
}
}, [])
useDidShow(() => {
reload()
});
})
if (list.length == 0) {
return (
<ConfigProvider>
<div className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 300px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="暂无数据"
/>
<Space>
<Button onClick={() => Taro.navigateTo({url: '/credit/creditMpCustomer/add'})}></Button>
</Space>
</div>
</ConfigProvider>
)
const goAdd = () => Taro.navigateTo({ url: '/credit/creditMpCustomer/add' })
const goEdit = (id?: number) => {
if (!id) return
Taro.navigateTo({ url: `/credit/creditMpCustomer/add?id=${id}` })
}
const onToggleRecommend = async (row: CreditMpCustomer) => {
if (!row?.id) return
const next = row.recommend === 1 ? 0 : 1
try {
await updateCreditMpCustomer({ ...row, recommend: next })
Taro.showToast({ title: next === 1 ? '已设为推荐' : '已取消推荐', icon: 'success' })
reload()
} catch (e) {
console.error('更新失败:', e)
Taro.showToast({ title: (e as any)?.message || '更新失败', icon: 'none' })
}
}
const onDel = async (id?: number) => {
if (!id) return
const res = await Taro.showModal({ title: '提示', content: '确认删除该记录?' })
if (!res.confirm) return
try {
await removeCreditMpCustomer(id)
Taro.showToast({ title: '删除成功', icon: 'success' })
reload()
} catch (e) {
console.error('删除失败:', e)
Taro.showToast({ title: (e as any)?.message || '删除失败', icon: 'none' })
}
}
return (
<>
{list.map((item, _) => (
<Cell.Group key={item.
<View className="bg-gray-50 min-h-screen">
<ConfigProvider>
<View className="px-3 pt-3">
<Space>
<Button type="primary" size="small" onClick={goAdd}>
</Button>
<Button size="small" loading={loading} onClick={reload}>
</Button>
</Space>
</View>
{!list.length ? (
<View className="px-3 pt-10">
<Empty description={loading ? '加载中...' : '暂无数据'} />
</View>
) : (
<View className="px-3 pt-3 pb-6">
<CellGroup>
{list.map(row => {
const recommended = row.recommend === 1
const title = row.toUser || '-'
const price = row.price ? `${row.price}` : '-'
const years = row.years ? `${row.years}` : '-'
const location = [row.province, row.city, row.region].filter(Boolean).join(' ')
const desc = `${price} · ${years}${location ? ` · ${location}` : ''}`
return (
<View key={row.id} onLongPress={() => onDel(row.id)}>
<Cell
title={title}
description={desc}
extra={
<View className="flex items-center gap-3">
<View
className="flex items-center"
onClick={e => {
e.stopPropagation()
onToggleRecommend(row)
}}
>
{recommended ? <Checked size={16} /> : <CheckNormal size={16} />}
<Text className="ml-1 text-xs text-gray-500">{recommended ? '推荐' : '未推荐'}</Text>
</View>
<ArrowRight color="#cccccc" />
</View>
}
onClick={() => goEdit(row.id)}
/>
</View>
)
})}
</CellGroup>
<View className="mt-2 text-xs text-gray-400">
</View>
</View>
)}
</ConfigProvider>
</View>
)
}