From 1d199c84414a427c358fc04a7266111431e6e6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 4 Mar 2026 11:04:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(credit):=20=E6=96=B0=E5=A2=9E=E4=BF=A1?= =?UTF-8?q?=E7=94=A8=E8=AE=A2=E5=8D=95=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加信用订单创建页面,支持填写拖欠方、金额、年数等信息 - 实现附件上传功能,支持图片和文档文件上传预览 - 集成城市选择组件,方便用户选择所在地区 - 添加服务协议勾选确认机制 - 在app配置中注册信用订单相关路由 - 重构文件上传API,新增uploadFileByPath方法支持路径上传 - 更新发现页面,集成店铺网点查询和定位功能 - 实现下拉刷新和无限滚动加载更多网点数据 - 添加地图导航和电话拨打功能 - 优化网点列表显示,按距离排序并显示距离信息 --- src/api/system/file/index.ts | 68 ++- src/app.config.ts | 7 + src/credit/order/add.config.ts | 5 + src/credit/order/add.scss | 63 +++ src/credit/order/add.tsx | 342 ++++++++++++ src/credit/order/index.config.ts | 5 + src/credit/order/index.tsx | 636 +++++++++++++++++++++++ src/pages/find/find.tsx | 310 +++++++---- src/pages/index/index.tsx | 7 +- src/pages/user/components/UserCard.tsx | 42 +- src/pages/user/components/UserFooter.tsx | 7 +- src/pages/user/components/UserGrid.tsx | 73 ++- src/pages/user/user.tsx | 9 +- 13 files changed, 1392 insertions(+), 182 deletions(-) create mode 100644 src/credit/order/add.config.ts create mode 100644 src/credit/order/add.scss create mode 100644 src/credit/order/add.tsx create mode 100644 src/credit/order/index.config.ts create mode 100644 src/credit/order/index.tsx diff --git a/src/api/system/file/index.ts b/src/api/system/file/index.ts index 0a86d92..38bdd42 100644 --- a/src/api/system/file/index.ts +++ b/src/api/system/file/index.ts @@ -56,6 +56,42 @@ const computeSignature = (accessKeySecret: string, canonicalString: string): str /** * 上传阿里云OSS */ +export async function uploadFileByPath(filePath: string) { + return new Promise((resolve: (result: FileRecord) => void, reject) => { + if (!filePath) { + reject(new Error('缺少 filePath')) + return + } + + // 统一走同一个上传接口:既支持图片,也支持文档等文件(由后端决定白名单/大小限制) + Taro.uploadFile({ + url: 'https://server.websoft.top/api/oss/upload', + filePath, + name: 'file', + header: { + 'content-type': 'application/json', + TenantId + }, + success: (res) => { + try { + const data = JSON.parse(res.data); + if (data.code === 0) { + resolve(data.data) + } else { + reject(new Error(data.message || '上传失败')) + } + } catch (_error) { + reject(new Error('解析响应数据失败')) + } + }, + fail: (err) => { + console.log('上传请求失败', err); + reject(new Error('上传请求失败')) + } + }) + }) +} + export async function uploadFile() { return new Promise(async (resolve: (result: FileRecord) => void, reject) => { Taro.chooseImage({ @@ -64,32 +100,12 @@ export async function uploadFile() { sourceType: ['album', 'camera'], success: async (res) => { const tempFilePath = res.tempFilePaths[0]; - // 上传图片到OSS - Taro.uploadFile({ - url: 'https://server.websoft.top/api/oss/upload', - filePath: tempFilePath, - name: 'file', - header: { - 'content-type': 'application/json', - TenantId - }, - success: (res) => { - try { - const data = JSON.parse(res.data); - if (data.code === 0) { - resolve(data.data) - } else { - reject(new Error(data.message || '上传失败')) - } - } catch (error) { - reject(new Error('解析响应数据失败')) - } - }, - fail: (err) => { - console.log('上传请求失败', err); - reject(new Error('上传请求失败')) - } - }) + try { + const record = await uploadFileByPath(tempFilePath) + resolve(record) + } catch (e) { + reject(e) + } }, fail: (err) => { console.log('选择图片失败', err); diff --git a/src/app.config.ts b/src/app.config.ts index 0c19a4c..3ae1b96 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -116,6 +116,13 @@ export default { "index", "article/index", ] + }, + { + "root": "credit", + "pages": [ + "order/index", + "order/add" + ] } ], window: { diff --git a/src/credit/order/add.config.ts b/src/credit/order/add.config.ts new file mode 100644 index 0000000..e3aa7f8 --- /dev/null +++ b/src/credit/order/add.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '发需求', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +}) diff --git a/src/credit/order/add.scss b/src/credit/order/add.scss new file mode 100644 index 0000000..5793f22 --- /dev/null +++ b/src/credit/order/add.scss @@ -0,0 +1,63 @@ +.credit-order-add-page { + padding-bottom: 100px; // leave space for FixedButton +} + +.attachments { + display: flex; + flex-direction: column; + gap: 12px; + + &__empty { + color: #999; + font-size: 14px; + } + + &__list { + display: flex; + flex-wrap: wrap; + gap: 10px; + } + + &__item { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border: 1px solid #eee; + border-radius: 8px; + background: #fafafa; + max-width: 100%; + } + + &__name { + font-size: 13px; + color: #333; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__remove { + color: #999; + display: inline-flex; + align-items: center; + } +} + +.agreement { + margin: 12px 16px 0; + display: flex; + align-items: center; + + &__text { + color: #666; + margin-left: 6px; + } + + &__link { + color: #1677ff; + margin-left: 4px; + } +} + diff --git a/src/credit/order/add.tsx b/src/credit/order/add.tsx new file mode 100644 index 0000000..fd27ab9 --- /dev/null +++ b/src/credit/order/add.tsx @@ -0,0 +1,342 @@ +import { useMemo, useRef, useState } from 'react' +import Taro from '@tarojs/taro' +import { View, Text } from '@tarojs/components' +import { Address, Button, Cell, CellGroup, Form, Input, Radio, TextArea } from '@nutui/nutui-react-taro' +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 './add.scss' + +type Attachment = { + id: string + name: string + url: string + isImage: boolean + thumbnail?: string +} + +const isHttpUrl = (url?: string) => { + if (!url) return false + return /^https?:\/\//i.test(url) +} + +export default function CreditOrderAddPage() { + const formRef = useRef(null) + + const [cityVisible, setCityVisible] = useState(false) + const [cityText, setCityText] = useState('') + + const [attachments, setAttachments] = useState([]) + const [uploading, setUploading] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [agree, setAgree] = useState(false) + + const cityOptions = useMemo(() => { + // NutUI Address options: [{ text, value, children }] + // @ts-ignore + return (RegionData || []).map(a => ({ + value: a.label, + text: a.label, + children: (a.children || []).map(b => ({ + value: b.label, + text: b.label, + children: (b.children || []).map(c => ({ + value: c.label, + text: c.label + })) + })) + })) + }, []) + + const addUploadedRecords = (incoming: Array<{ url?: string; thumbnail?: string; name?: string }>, opts: { isImage: boolean }) => { + const next = incoming + .map((r, idx) => { + const url = String(r.url || r.thumbnail || '').trim() + if (!url) return null + const name = String(r.name || (opts.isImage ? `图片${idx + 1}` : `文件${idx + 1}`)).trim() + const id = `${Date.now()}-${Math.random().toString(16).slice(2)}` + return { id, name, url, thumbnail: r.thumbnail, isImage: opts.isImage } as Attachment + }) + .filter(Boolean) as Attachment[] + + if (!next.length) return + setAttachments(prev => prev.concat(next)) + } + + const chooseAndUploadImages = async () => { + const res = await Taro.chooseImage({ + count: 9, + sizeType: ['compressed'], + sourceType: ['album', 'camera'] + }) + 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 }) + Taro.showToast({ title: '上传成功', icon: 'success' }) + } catch (e) { + console.error('上传图片失败:', e) + Taro.showToast({ title: '上传失败,请重试', icon: 'none' }) + } finally { + setUploading(false) + } + } + + const chooseAndUploadDocs = async () => { + // 微信小程序:从会话/聊天等入口选择文件 + // H5 等环境可能不支持,走 try/catch 提示即可 + // @ts-ignore + const res = await Taro.chooseMessageFile({ + count: 9, + type: 'file' + }) + // @ts-ignore + const tempFiles = (res?.tempFiles || []) as Array<{ path?: string; name?: string }> + const paths = tempFiles.map(f => f?.path).filter(Boolean) as string[] + if (!paths.length) return + + setUploading(true) + try { + const uploaded = [] + for (let i = 0; i < paths.length; i++) { + const record = await uploadFileByPath(paths[i]) + const name = tempFiles[i]?.name + uploaded.push({ ...(record as any), name: name || (record as any)?.name }) + } + addUploadedRecords(uploaded, { isImage: false }) + Taro.showToast({ title: '上传成功', icon: 'success' }) + } catch (e) { + console.error('上传文件失败:', e) + Taro.showToast({ title: '上传失败,请重试', icon: 'none' }) + } finally { + setUploading(false) + } + } + + const chooseAttachment = async () => { + try { + const res = await Taro.showActionSheet({ + itemList: ['上传图片', '上传文件'] + }) + if (res.tapIndex === 0) await chooseAndUploadImages() + if (res.tapIndex === 1) await chooseAndUploadDocs() + } catch (e) { + // 用户取消 + const msg = String((e as any)?.errMsg || (e as any)?.message || e || '') + if (msg.includes('cancel')) return + console.error('选择上传类型失败:', e) + } + } + + const removeAttachment = (id: string) => { + setAttachments(prev => prev.filter(a => a.id !== id)) + } + + const previewAttachment = async (a: Attachment) => { + try { + if (a.isImage) { + const imgs = attachments.filter(x => x.isImage).map(x => x.url) + await Taro.previewImage({ urls: imgs, current: a.url }) + return + } + + let filePath = a.url + if (isHttpUrl(a.url)) { + const dl = await Taro.downloadFile({ url: a.url }) + // @ts-ignore + filePath = dl?.tempFilePath + } + + await Taro.openDocument({ filePath, showMenu: true }) + } catch (e) { + console.error('预览文件失败:', e) + Taro.showToast({ title: '无法打开该文件', icon: 'none' }) + } + } + + const submitSucceed = async (values: any) => { + const payer = String(values?.payer || '').trim() + const amount = Number(values?.amount) + const years = Number(values?.years) + const remark = String(values?.remark || '').trim() + + if (!payer) { + Taro.showToast({ title: '请输入拖欠方(付款方)', icon: 'none' }) + return + } + if (!Number.isFinite(amount) || amount <= 0) { + Taro.showToast({ title: '请输入正确的拖欠金额', icon: 'none' }) + return + } + if (!Number.isFinite(years) || years <= 0) { + Taro.showToast({ title: '请输入正确的拖欠年数', icon: 'none' }) + return + } + if (!agree) { + Taro.showToast({ title: '请先同意服务协议', icon: 'none' }) + return + } + + const payload = { + payer, + amount, + years, + remark, + city: cityText, + files: attachments.map(a => ({ name: a.name, url: a.url, isImage: a.isImage })) + } + + setSubmitting(true) + try { + // TODO: 这里可替换为真实接口提交(如后端提供 `/credit/order`) + console.log('发需求提交:', payload) + Taro.showToast({ title: '提交成功', icon: 'success' }) + Taro.eventCenter.trigger('credit:order:created', payload) + setTimeout(() => { + Taro.navigateBack() + }, 800) + } catch (e) { + console.error('提交失败:', e) + Taro.showToast({ title: '提交失败,请重试', icon: 'none' }) + } finally { + setSubmitting(false) + } + } + + const submitFailed = (err: any) => { + console.log('表单校验失败:', err) + } + + const canSubmit = !uploading && !submitting + + return ( + +
+ + + + + + + + + + + + + + +