From 372275ef448b9bf9417f95bffd7a887a4e5e3048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Tue, 17 Mar 2026 22:16:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(credit):=20=E6=96=B0=E5=A2=9E=E6=88=91?= =?UTF-8?q?=E7=9A=84=E8=AE=A2=E5=8D=95=E6=9F=A5=E8=AF=A2=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 credit/my-order/index 页面用于订单进度查询 - 在 app.config.ts 中注册新的订单查询页面路由 - 修改文件上传逻辑,统一使用 uploadFile 方法替代手动选择图片流程 - 重构 uploadFileByPath 函数,增强错误处理和响应解析逻辑 - 修复STS token过期判断条件,确保及时刷新临时凭证 - 实现订单列表的搜索、筛选、排序和分页加载功能 - 添加日期范围选择器和回款状态过滤功能 - 优化图片上传用户体验,与用户认证页面保持一致 --- src/api/system/file/index.ts | 84 ++++--- src/app.config.ts | 4 +- src/credit/my-order/index.config.ts | 5 + src/credit/my-order/index.tsx | 348 ++++++++++++++++++++++++++++ src/credit/order/add.tsx | 29 +-- 5 files changed, 413 insertions(+), 57 deletions(-) create mode 100644 src/credit/my-order/index.config.ts create mode 100644 src/credit/my-order/index.tsx diff --git a/src/api/system/file/index.ts b/src/api/system/file/index.ts index 2411108..85ff2f8 100644 --- a/src/api/system/file/index.ts +++ b/src/api/system/file/index.ts @@ -4,7 +4,7 @@ import dayjs from 'dayjs'; import crypto from 'crypto-js'; import {Base64} from 'js-base64'; import {FileRecord} from "@/api/system/file/model"; -import {TenantId} from "@/config/app"; +import { TenantId } from "@/config/app"; export async function uploadOssByPath(filePath: string) { return new Promise(async (resolve) => { @@ -19,7 +19,7 @@ export async function uploadOssByPath(filePath: string) { }; let sts = Taro.getStorageSync('sts'); let stsExpired = Taro.getStorageSync('stsExpiredAt'); - if (!sts || (stsExpired && dayjs().isBefore(dayjs(stsExpired)))) { + if (!sts || (stsExpired && dayjs().isAfter(dayjs(stsExpired)))) { // @ts-ignore const {data: {data: {credentials}}} = await request.get(`https://gle-server.websoft.top/api/oss/getSTSToken`) Taro.setStorageSync('sts', credentials) @@ -57,20 +57,58 @@ const computeSignature = (accessKeySecret: string, canonicalString: string): str * 上传阿里云OSS */ export async function uploadFileByPath(filePath: string) { + const parseResponseToRecord = (res: any): FileRecord => { + const statusCode = Number(res?.statusCode) + if (Number.isFinite(statusCode) && statusCode !== 200) { + throw new Error(`上传失败(HTTP ${statusCode})`) + } + + const raw = res?.data + if (raw === null || raw === undefined || raw === '') { + throw new Error('上传失败:响应为空') + } + + let data: any + if (typeof raw === 'string') { + const cleaned = raw.replace(/^\uFEFF/, '').trim() + try { + data = JSON.parse(cleaned) + } catch (_e) { + throw new Error('上传失败:响应格式错误') + } + } else { + data = raw + } + + if (data?.code === 0) return data.data as FileRecord + + const codeHint = data?.code !== undefined && data?.code !== null ? `(code=${String(data.code)})` : '' + let msg = String(data?.message || data?.msg || data?.error || data?.errMsg || `上传失败${codeHint}`) + try { + msg = decodeURIComponent(escape(msg)) + } catch (_e) { + // ignore + } + throw new Error(msg || '上传失败') + } + + const tenantIdInStorage = Taro.getStorageSync('TenantId') + const tenantId = + tenantIdInStorage && String(tenantIdInStorage) === String(TenantId) + ? tenantIdInStorage + : TenantId + + const header: Record = { + 'content-type': 'application/json', + TenantId: String(tenantId) + } + return new Promise((resolve: (result: FileRecord) => void, reject) => { if (!filePath) { reject(new Error('缺少 filePath')) return } - const tenantId = Taro.getStorageSync('TenantId') || TenantId - - const header: Record = { - 'content-type': 'application/json', - TenantId: String(tenantId) - } - - // 统一走同一个上传接口:既支持图片,也支持文档等文件(由后端决定白名单/大小限制) Taro.uploadFile({ url: 'https://server.websoft.top/api/oss/upload', filePath, @@ -78,30 +116,14 @@ export async function uploadFileByPath(filePath: string) { header, success: (res) => { try { - if ((res as any)?.statusCode && (res as any).statusCode !== 200) { - reject(new Error(`上传失败(HTTP ${(res as any).statusCode})`)) - return - } - const raw = (res as any)?.data - const data = typeof raw === 'string' ? JSON.parse(raw) : raw - if (data.code === 0) { - resolve(data.data) - } else { - let msg = String(data.message || '上传失败') - try { - msg = decodeURIComponent(escape(msg)) - } catch (_e) { - // ignore - } - reject(new Error(msg || '上传失败')) - } - } catch (_error) { - reject(new Error('解析响应数据失败')) + resolve(parseResponseToRecord(res)) + } catch (e) { + reject(e) } }, fail: (err) => { - console.log('上传请求失败', err); - reject(new Error('上传请求失败')) + const msg = String((err as any)?.errMsg || (err as any)?.message || '上传请求失败') + reject(new Error(msg)) } }) }) diff --git a/src/app.config.ts b/src/app.config.ts index f4cc0fa..ed9449e 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -119,7 +119,8 @@ export default { }, { "root": "credit", - "pages": ["data/index", + "pages": [ + "data/index", "customer/index", "order/index", "order/add", @@ -128,6 +129,7 @@ export default { "company/detail", "company/follow-step1", "company/edit", + "my-order/index", 'creditMpCustomer/index', 'creditMpCustomer/add',] } diff --git a/src/credit/my-order/index.config.ts b/src/credit/my-order/index.config.ts new file mode 100644 index 0000000..6c82ccf --- /dev/null +++ b/src/credit/my-order/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '查询进度', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +}) diff --git a/src/credit/my-order/index.tsx b/src/credit/my-order/index.tsx new file mode 100644 index 0000000..f8eb5d0 --- /dev/null +++ b/src/credit/my-order/index.tsx @@ -0,0 +1,348 @@ +import { useMemo, useState } from 'react' +import Taro from '@tarojs/taro' +import { View, Text } from '@tarojs/components' +import { Button, ConfigProvider, DatePicker, Empty, Input, Popup } from '@nutui/nutui-react-taro' +import { Search } from '@nutui/icons-react-taro' +import dayjs from 'dayjs' + +type PayStatus = '全部回款' | '部分回款' | '未回款' +type AmountSort = '默认' | '从高到低' | '从低到高' + +type OrderItem = { + id: string + orderNo: string + companyName: string + unifiedCode: string + projectName: string + follower: string + payStatus: PayStatus + date: string // YYYY-MM-DD + principal: number + interest: number +} + +const formatYmd = (d?: Date | null) => { + if (!d) return '' + return dayjs(d).format('YYYY-MM-DD') +} + +const getStatusStyle = (s: PayStatus) => { + if (s === '全部回款') return 'bg-green-500' + if (s === '部分回款') return 'bg-orange-500' + return 'bg-red-500' +} + +const makeMockOrders = (page: number): OrderItem[] => { + const companies = [ + { name: '广西万宇工程建设有限公司', code: '91450100MA00000001' }, + { name: '广西远恒建筑有限公司', code: '91450100MA00000002' }, + { name: '南宁宏达工程有限公司', code: '91450100MA00000003' }, + { name: '桂林鑫盛建设有限公司', code: '91450100MA00000004' } + ] + const followers = ['罗天东', '张三', '李四', '王五'] + const suffix = ['一期', '二期', '改扩建', '配套', '市政', '装饰', '机电'] + + // page=1:给出 16 条示例,统计更直观(项目=订单维度,而非公司维度) + const size = page === 1 ? 16 : 6 + const baseNo = 9099009999 + (page - 1) * 100 + + const list: OrderItem[] = [] + for (let i = 0; i < size; i++) { + const c = companies[(page + i) % companies.length] + const follower = followers[(i + page) % followers.length] + const payStatus: PayStatus = + page === 1 ? '全部回款' : (['全部回款', '部分回款', '未回款'][(i + page) % 3] as PayStatus) + + const orderNo = String(baseNo + i) + const date = dayjs('2025-10-10').subtract((page - 1) * 7 + (i % 6), 'day').format('YYYY-MM-DD') + + // page=1:让本金/利息合计更规整(本金 2000,利息 200) + const principal = page === 1 ? (i < 8 ? 100 : 150) : 200 + ((i + page) % 5) * 50 + const interest = page === 1 ? (i < 8 ? 10 : 15) : 20 + ((i + page) % 4) * 5 + + const projectName = `${c.name}${suffix[(i + page) % suffix.length]}项目` + + list.push({ + id: `${page}-${i}-${orderNo}`, + orderNo, + companyName: c.name, + unifiedCode: c.code, + projectName, + follower, + payStatus, + date, + principal, + interest + }) + } + return list +} + +export default function CreditOrderPage() { + + const [rawList, setRawList] = useState(() => makeMockOrders(1)) + const [mockPage, setMockPage] = useState(1) + + const [searchValue, setSearchValue] = useState('') + const [amountSort, setAmountSort] = useState('默认') + const [payFilter, setPayFilter] = useState('全部回款') + + const [datePopupVisible, setDatePopupVisible] = useState(false) + const [datePickerVisible, setDatePickerVisible] = useState(false) + const [picking, setPicking] = useState<'start' | 'end'>('start') + const [startDate, setStartDate] = useState(null) + const [endDate, setEndDate] = useState(null) + + const filteredList = useMemo(() => { + let list = rawList.slice() + + const q = searchValue.trim() + if (q) { + const qq = q.toLowerCase() + list = list.filter(o => { + return ( + String(o.companyName || '').toLowerCase().includes(qq) || + String(o.unifiedCode || '').toLowerCase().includes(qq) || + String(o.orderNo || '').toLowerCase().includes(qq) + ) + }) + } + + if (payFilter) { + list = list.filter(o => o.payStatus === payFilter) + } + + if (startDate || endDate) { + const start = startDate ? dayjs(startDate).startOf('day') : null + const end = endDate ? dayjs(endDate).endOf('day') : null + list = list.filter(o => { + const t = dayjs(o.date, 'YYYY-MM-DD') + if (start && t.isBefore(start)) return false + if (end && t.isAfter(end)) return false + return true + }) + } + + if (amountSort !== '默认') { + list.sort((a, b) => { + const av = Number(a.principal || 0) + const bv = Number(b.principal || 0) + return amountSort === '从高到低' ? bv - av : av - bv + }) + } + + return list + }, [amountSort, endDate, payFilter, rawList, searchValue, startDate]) + + const stats = useMemo(() => { + // 业务说明:一个公司可能对应多个项目(多条订单),因此“订单数量”=项目/订单条数,而不是公司数。 + const total = filteredList.length + const principal = filteredList.reduce((sum, o) => sum + Number(o.principal || 0), 0) + const interest = filteredList.reduce((sum, o) => sum + Number(o.interest || 0), 0) + return { total, principal, interest } + }, [filteredList]) + + const timeText = useMemo(() => { + const s = formatYmd(startDate) + const e = formatYmd(endDate) + if (!s && !e) return '时间筛选' + if (s && e) return `${s}~${e}` + return s ? `${s}~` : `~${e}` + }, [endDate, startDate]) + + const pickAmountSort = async () => { + try { + const options: AmountSort[] = ['默认', '从高到低', '从低到高'] + const res = await Taro.showActionSheet({ itemList: options }) + const next = options[res.tapIndex] + if (next) setAmountSort(next) + } catch (e) { + const msg = String((e as any)?.errMsg || (e as any)?.message || e || '') + if (msg.includes('cancel')) return + console.error('选择金额排序失败:', e) + } + } + + const pickPayFilter = async () => { + try { + const options: PayStatus[] = ['全部回款', '部分回款', '未回款'] + const res = await Taro.showActionSheet({ itemList: options }) + const next = options[res.tapIndex] + if (next) setPayFilter(next) + } catch (e) { + const msg = String((e as any)?.errMsg || (e as any)?.message || e || '') + if (msg.includes('cancel')) return + console.error('选择回款筛选失败:', e) + } + } + + const loadMore = () => { + const nextPage = mockPage + 1 + const incoming = makeMockOrders(nextPage) + setMockPage(nextPage) + setRawList(prev => prev.concat(incoming)) + console.log('加载更多:', { nextPage, appended: incoming.length }) + Taro.showToast({ title: '已加载更多', icon: 'none' }) + } + + return ( + + + + + + + + + + + + + + 金额排序 + {amountSort === '从高到低' ? '↓' : amountSort === '从低到高' ? '↑' : '↕'} + + + {payFilter} + + setDatePopupVisible(true)} + > + {timeText} + + + + + 总订单量:{stats.total}个 + 订单本金:{stats.principal}元 + 利息金额:{stats.interest}元 + + + + + {!filteredList.length ? ( + + + + ) : ( + filteredList.map(o => ( + + + 订单号:{o.orderNo} + + {o.payStatus} + + + + {/* 项目名称是核心分组/统计维度(一个公司可有多个项目=多条订单),因此需要突出显示 */} + + {o.projectName} + + + + 公司:{o.companyName} + 跟进人:{o.follower} + 日期:{o.date} + + + + + 本金: + {o.principal}元 + + + 利息: + {o.interest}元 + + + + )) + )} + + + + + + + + setDatePopupVisible(false)} + style={{ borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }} + > + + 时间筛选 + + { setPicking('start'); setDatePickerVisible(true) }}> + 开始日期 + {formatYmd(startDate) || '未选择'} + + { setPicking('end'); setDatePickerVisible(true) }}> + 结束日期 + {formatYmd(endDate) || '未选择'} + + + + + + + + + + + setDatePickerVisible(false)} + onCancel={() => setDatePickerVisible(false)} + onConfirm={(_options, selectedValue) => { + const [y, m, d] = (selectedValue || []).map(v => Number(v)) + const next = new Date(y, (m || 1) - 1, d || 1, 0, 0, 0) + + if (picking === 'start') { + setStartDate(next) + if (endDate && dayjs(endDate).isBefore(next, 'day')) setEndDate(null) + } else { + setEndDate(next) + if (startDate && dayjs(next).isBefore(startDate, 'day')) setStartDate(null) + } + setDatePickerVisible(false) + }} + /> + + + ) +} diff --git a/src/credit/order/add.tsx b/src/credit/order/add.tsx index a55fd2e..531b6fa 100644 --- a/src/credit/order/add.tsx +++ b/src/credit/order/add.tsx @@ -6,7 +6,7 @@ import { ArrowRight, Close } from '@nutui/icons-react-taro' import FixedButton from '@/components/FixedButton' import RegionData from '@/api/json/regions-data.json' -import { uploadFileByPath } from '@/api/system/file' +import { uploadFile, uploadFileByPath } from '@/api/system/file' import { addCreditMpCustomer } from '@/api/credit/creditMpCustomer' import './add.scss' @@ -79,32 +79,11 @@ export default function CreditOrderAddPage() { } const chooseAndUploadImages = async () => { - let res: any - try { - res = await Taro.chooseImage({ - count: 9, - sizeType: ['compressed'], - sourceType: ['album', 'camera'] - }) - } catch (e) { - const msg = String((e as any)?.errMsg || (e as any)?.message || e || '') - if (msg.includes('cancel')) return - console.error('选择图片失败:', e) - Taro.showToast({ title: '选择图片失败', icon: 'none' }) - return - } - - const paths = (res?.tempFilePaths || []).filter(Boolean) - if (!paths.length) return - setUploading(true) try { - const uploaded = [] - for (const p of paths) { - const record = await uploadFileByPath(p) - uploaded.push(record as any) - } - addUploadedRecords(uploaded, { isImage: true }) + // 与用户认证页一致:走 uploadFile() 内部的 chooseImage + uploadFileByPath 流程 + const record = await uploadFile() + addUploadedRecords([record as any], { isImage: true }) Taro.showToast({ title: '上传成功', icon: 'success' }) } catch (e) { console.error('上传图片失败:', e)