feat:新增功能和接口对接

This commit is contained in:
5g0Wp7Zy 2025-11-18 17:05:58 +08:00
parent 56d38e8f3d
commit 0793b41818
16 changed files with 727 additions and 72 deletions

2
env/.env vendored
View File

@ -8,7 +8,7 @@ VITE_WX_APPID = 'wx327d788d7bd6eddf'
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
# 比如你要部署到 https://unibest.tech/doc/ ,则配置为 /doc/
VITE_APP_PUBLIC_BASE=/
# http://192.168.101.18:10010
# 后台请求地址
VITE_SERVER_BASEURL = 'http://192.168.101.18:10010'
# 备注如果后台带统一前缀则也要加到后面eg: https://ukw0y1.laf.run/api

View File

@ -1,9 +1,12 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { navigateToInterceptor } from '@/router/interceptor'
import { useSystemStore } from './store'
onLaunch((options) => {
console.log('App Launch', options)
console.log('App Launch213123', options)
const systemStore = useSystemStore()
systemStore.setDicsValue()
})
onShow((options) => {
console.log('App Show', options)

9
src/api/malfunction.ts Normal file
View File

@ -0,0 +1,9 @@
import type { IMalfunctionReport } from './types/malfunction'
import { http } from '@/http/http'
// 保存用户故障上报信息
export function saveMalfunctionAPI(data: IMalfunctionReport) {
return http.post<any>('/user/ebikeFaultReport/save', data, {
noToken: true,
})
}

View File

@ -1,4 +1,4 @@
import type { FinishRideParams, StartRideParams, UserOrdersQuery } from './types/order'
import type { FinishRideParams, RefundRequestParams, StartRideParams, UserOrdersQuery } from './types/order'
import { http } from '@/http/http'
// 开始骑行(生成订单、开锁)
@ -58,3 +58,11 @@ export function getOrderDetailAPI(orderId: string) {
noToken: true,
})
}
// 退款申请
export function applyRefundAPI(data: RefundRequestParams) {
return http.post<any>('/user/ebikeRefund/refundApply', data, {
noToken: true,
hideErrorToast: true,
})
}

8
src/api/system.ts Normal file
View File

@ -0,0 +1,8 @@
import { http } from '@/http/http'
// 获取字典和字典值
export function getDictsAndValuesAPI() {
return http.get<any>('/user/ebikeDicValue/list', undefined, {
noToken: true,
})
}

View File

@ -0,0 +1,13 @@
// 保存用户故障上报信息
export interface IMalfunctionReport {
userId: string
bikeCode: string
faultPart: number[]
faultDescription: string
location: {
type: 'polygon' | 'point'
longitude: number
latitude: number
}
attachmentFiles: any
}

View File

@ -39,3 +39,11 @@ export interface UserOrdersQuery {
pageNum: number
pageSize: number
}
// 退款申请
export interface RefundRequestParams {
orderId: string
problemType: number
problemDescription: string
refundFiles?: any[]
}

99
src/api/upload.ts Normal file
View File

@ -0,0 +1,99 @@
// 获取环境变量
const { VITE_SERVER_BASEURL } = import.meta.env
// 故障上传接口
export const faultUploadUrl = `${VITE_SERVER_BASEURL}/user/ebikeFaultReport/uploadFile`
// 退款上传接口
export const refundUploadUrl = `${VITE_SERVER_BASEURL}/user/ebikeRefund/uploadFile`
/**
* URL数组下载并上传图片
* @param {string[]} urls - URL数组 ["http://...", "http://..."]
* @param {string} uploadUrl -
* @param {object} [options] -
* @param {string} [options.name] - key
* @param {object} [options.formData] -
* @param {object} [options.header] -
* @returns {Promise<Array>} -
*/
export async function uploadImagesStrict(urls: string[], uploadUrl: string, options: any = {}) {
if (!Array.isArray(urls) || urls.length === 0) {
throw new Error('urls 必须是非空数组')
}
const {
name = 'file',
formData = {},
header = {},
} = options
// 下载单张图片 → 返回本地临时路径
async function downloadImage(url: string) {
return new Promise((resolve, reject) => {
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.tempFilePath)
}
else {
reject(new Error(`下载失败 [${url}],状态码: ${res.statusCode}`))
}
},
fail: (err) => {
reject(new Error(`下载失败 [${url}]${err.errMsg || err.message}`))
},
})
})
}
// 上传单张图片
async function uploadImage(localPath: any) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: uploadUrl,
filePath: localPath,
name,
formData,
header,
success: (res) => {
// 注意uni.uploadFile 的 success 不代表业务成功!
// 你可能需要解析 res.data 并判断业务状态
console.log(res.data)
console.log(JSON.parse(res.data))
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
// 假设后端返回 { code: 200, data: ... } 表示成功
if (data.code === 200 || res.statusCode === 200) {
resolve(data.data)
}
else {
reject(new Error(`上传失败:${data.msg || '未知错误'}`))
}
}
catch (e) {
// 如果无法解析 JSON也视为失败
reject(new Error(`上传响应解析失败:${res.data}`))
}
},
fail: (err: any) => {
reject(new Error(`上传请求失败:${err.errMsg || err.message}`))
},
})
})
}
// 并发下载所有图片
const localPaths = await Promise.all(
urls.map(url => downloadImage(url)),
)
// 并发上传所有图片
const uploadResults = await Promise.all(
localPaths.map(path => uploadImage(path)),
)
return uploadResults // 全部成功才返回结果数组
}

View File

@ -1,4 +1,8 @@
<script lang="ts" setup>
import { saveMalfunctionAPI } from '@/api/malfunction'
import { faultUploadUrl, uploadImagesStrict } from '@/api/upload'
import { useSystemStore, useUserStore } from '@/store'
definePage({
style: {
navigationBarTitleText: '故障反馈',
@ -7,83 +11,94 @@ definePage({
},
})
const userStore = useUserStore()
const systemStore = useSystemStore()
const form = ref<any>(null)
const formData = reactive({
bikeCode: '',
faultDescription: '',
faultPart: '',
faultPart: [],
attachmentFiles: [],
location: {
type: 'point' as const,
latitude: 0,
longitude: 0,
},
})
const fault_list = ref([
{
icon: 'cheba',
name: '车把',
selected: false,
},
{
icon: 'motuochetou',
name: '车头',
selected: false,
},
{
icon: 'gaoliangchedeng',
name: '车灯',
selected: false,
},
{
icon: 'ico',
name: '二维码',
selected: false,
},
{
icon: 'gouwuche',
name: '车筐',
selected: false,
},
{
icon: 'luxian',
name: '线路',
selected: false,
},
{
icon: 'chezuo',
name: '车座',
selected: false,
},
{
icon: 'dianchi',
name: '电池',
selected: false,
},
{
icon: 'chelun',
name: '车轮',
selected: false,
},
{
icon: 'izuliao',
name: '脚蹬',
selected: false,
},
{
icon: 'qichechenggan',
name: '车撑',
selected: false,
},
{
icon: 'zhedangban-23',
name: '挡板',
selected: false,
},
// {
// icon: 'cheba',
// name: '',
// selected: false,
// },
// {
// icon: 'motuochetou',
// name: '',
// selected: false,
// },
// {
// icon: 'gaoliangchedeng',
// name: '',
// selected: false,
// },
// {
// icon: 'ico',
// name: '',
// selected: false,
// },
// {
// icon: 'gouwuche',
// name: '',
// selected: false,
// },
// {
// icon: 'luxian',
// name: '线',
// selected: false,
// },
// {
// icon: 'chezuo',
// name: '',
// selected: false,
// },
// {
// icon: 'dianchi',
// name: '',
// selected: false,
// },
// {
// icon: 'chelun',
// name: '',
// selected: false,
// },
// {
// icon: 'izuliao',
// name: '',
// selected: false,
// },
// {
// icon: 'qichechenggan',
// name: '',
// selected: false,
// },
// {
// icon: 'zhedangban-23',
// name: '',
// selected: false,
// },
])
const fileList = ref([])
function handleClick(item: any, index: number) {
uni.vibrateShort()
fault_list.value[index].selected = !item.selected
}
function afterRead(event: any) {
async function afterRead(event: any) {
const lists = [].concat(event.file)
let fileListLen = fileList.value.length
lists.forEach((item) => {
fileList.value.push({
...item,
@ -91,11 +106,88 @@ function afterRead(event: any) {
message: '上传中',
})
})
for (let i = 0; i < lists.length; i++) {
const item = fileList.value[fileListLen]
try {
const result: any = await uploadImagesStrict([lists[i].url], faultUploadUrl, {
name: 'multipartFile',
header: {
'Content-Type': 'multipart/form-data',
},
})
formData.attachmentFiles.push(result[0])
fileList.value.splice(fileListLen, 1, Object.assign(item, {
status: 'success',
message: '',
url: result[0].fileUrl,
}))
fileListLen++
}
catch (err) {
fileList.value.splice(fileListLen, 1, Object.assign(item, {
status: 'failed',
message: '',
url: '',
}))
}
}
}
function deletePic() {
function deletePic(e: any) {
fileList.value.splice(e.index, 1)
formData.attachmentFiles.splice(e.index, 1)
}
async function submit() {
console.log('提交表单', formData)
try {
const selectedFaults = fault_list.value
.filter(item => item.selected)
.map(item => item.value)
console.log(selectedFaults)
const res = await saveMalfunctionAPI({
userId: userStore.userInfo.userId,
faultPart: selectedFaults,
faultDescription: formData.faultDescription,
bikeCode: formData.bikeCode,
attachmentFiles: formData.attachmentFiles,
location: formData.location,
})
}
catch (err) {
console.error('提交失败', err)
}
}
onLoad(async () => {
const list = await systemStore.getDictValue('inventory_type')
console.log(list)
if (list?.length > 0) {
fault_list.value = list.map((item: any) => ({
icon: item.dicIcon || '',
name: item.dicValueName,
value: Number(item.dicValue),
selected: false,
}))
}
})
onShow(() => {
uni.getLocation({
type: 'gcj02',
success(res) {
console.log(res, '地理位置')
formData.location.latitude = res.latitude
formData.location.longitude = res.longitude
},
fail(err) {
console.error('获取位置失败:', err)
},
})
})
</script>
<template>
@ -165,6 +257,7 @@ function deletePic() {
<uv-button
type="primary"
text="上报故障"
@click="submit"
/>
</view>
</template>

View File

@ -73,6 +73,12 @@ function drawLine() {
startAndEndMapRef.value && startAndEndMapRef.value.adjustMapView(points.value)
}
function returnFee() {
uni.navigateTo({
url: `/pages-sub/refundFee/refundFee?orderId=${orderIds.value}`,
})
}
onLoad((query) => {
const { orderId } = query
if (orderId) {
@ -115,7 +121,7 @@ onMounted(() => {
<view class="price">
已支付{{ toFixedString(orderDetail.actualAmount, 2) }}
</view>
<view>
<view class="icon_box">
<view class="icon_bar">
<uv-icon
size="24"
@ -127,6 +133,17 @@ onMounted(() => {
故障上报
</view>
</view>
<view class="icon_bar" style="margin-left: 30rpx;" @click="returnFee">
<uv-icon
size="24"
name="shenqingtuikuan"
color="#5f5f61"
custom-prefix="custom-icon"
/>
<view style="margin-top: 8rpx; color: #5f5f61;">
申请退款
</view>
</view>
</view>
</view>
<view class="middle_info">
@ -175,11 +192,16 @@ onMounted(() => {
font-weight: bold;
}
.icon_bar {
.icon_box {
display: flex;
flex-direction: column;
align-items: center;
font-size: 24rpx;
.icon_bar {
display: flex;
flex-direction: column;
align-items: center;
font-size: 24rpx;
}
}
}

View File

@ -15,6 +15,9 @@ const userStore = useUserStore()
const list = ref<any>([])
const paging = ref<any>(null)
async function queryList(pageNum: number, pageSize: number) {
uni.showLoading({
title: '加载中...',
})
try {
const res = await getUserOrdersAPI({
userId: userStore.userInfo.userId,
@ -24,9 +27,15 @@ async function queryList(pageNum: number, pageSize: number) {
console.log(res)
const { records } = res
paging.value.complete(records)
uni.hideLoading()
}
catch (err) {
console.error('Error fetching user orders:', err)
uni.showToast({
title: '加载失败,请稍后重试',
icon: 'error',
})
uni.hideLoading()
}
}

View File

@ -0,0 +1,305 @@
<script lang="ts" setup>
import { applyRefundAPI } from '@/api/order'
import { refundUploadUrl, uploadImagesStrict } from '@/api/upload'
import { useSystemStore, useUserStore } from '@/store'
definePage({
style: {
navigationBarTitleText: '申请退款',
navigationBarBackgroundColor: '#1488f5',
navigationBarTextStyle: 'white',
},
})
const userStore = useUserStore()
const systemStore = useSystemStore()
const fileList = ref([])
const problemTypeList = ref([])
const formData = reactive({
problemDescription: '',
refundFiles: [],
problemType: undefined,
orderId: '',
})
const btnConfig = reactive({
text: '提交申请',
loadingText: '提交中...',
disabledText: '已提交',
loading: false,
disabled: false,
})
async function afterRead(event: any) {
const lists = [].concat(event.file)
let fileListLen = fileList.value.length
lists.forEach((item) => {
fileList.value.push({
...item,
status: 'uploading',
message: '上传中',
})
})
for (let i = 0; i < lists.length; i++) {
const item = fileList.value[fileListLen]
try {
const result: any = await uploadImagesStrict([lists[i].url], refundUploadUrl, {
name: 'multipartFile',
header: {
'Content-Type': 'multipart/form-data',
},
})
formData.refundFiles.push(result[0])
fileList.value.splice(fileListLen, 1, Object.assign(item, {
status: 'success',
message: '',
url: result[0].fileUrl,
}))
fileListLen++
}
catch (err) {
fileList.value.splice(fileListLen, 1, Object.assign(item, {
status: 'failed',
message: '',
url: '',
}))
}
}
}
function deletePic(e: any) {
fileList.value.splice(e.index, 1)
formData.refundFiles.splice(e.index, 1)
}
function handleClick(item: any, index: number) {
uni.vibrateShort()
formData.problemType = item.dicValue
}
//
function resetFormatData() {
formData.orderId = ''
formData.problemType = undefined
formData.problemDescription = ''
formData.refundFiles = []
fileList.value = []
}
// 退
async function submit() {
btnConfig.loading = true
try {
const res = await applyRefundAPI(formData)
btnConfig.loading = false
uni.showToast({
title: '提交成功',
icon: 'success',
})
setTimeout(() => {
resetFormatData()
uni.navigateBack()
}, 800)
}
catch (err) {
btnConfig.loading = false
console.error('提交退款申请失败:', err)
uni.showToast({
title: '提交失败',
icon: 'error',
})
}
}
onLoad(async (query) => {
const { orderId } = query
if (orderId) {
formData.orderId = orderId
}
const list = await systemStore.getDictValue('problem_Type')
if (list?.length > 0) {
problemTypeList.value = list.map((item: any) => ({
...item,
dicValue: Number(item.dicValue),
}))
}
})
</script>
<template>
<z-paging
ref="paging"
>
<view class="container">
<!-- 退款描述 -->
<view class="describe_tips">
如果您对此单有异议请提交相关信息我们核实后会退回多扣的费用
</view>
<!-- 问题类型 -->
<view class="problemTypes">
<view class="titleup">
问题类型
</view>
<view class="type_Bar">
<view
v-for="(item, index) in problemTypeList"
:key="item.dicId"
class="type_item"
:class="[item.dicValue === formData.problemType ? 'type_item_active' : '']"
@click="handleClick(item, index)"
>
{{ item.dicValueName }}
</view>
</view>
</view>
<!-- 其他问题 -->
<view class="oher_question">
<view class="titleup" style="margin-bottom: 20rpx;">
问题描述
</view>
<uv-textarea v-model="formData.problemDescription" count :maxlength="200" placeholder="请描述" />
</view>
<!-- 上传图片 -->
<view class="upload-img">
<view class="titleup" style="margin-bottom: 20rpx;">
拍摄照片
</view>
<uv-upload
:file-list="fileList"
name="1"
multiple
:preview-full-image="true"
@after-read="afterRead"
@delete="deletePic"
/>
</view>
</view>
<template #bottom>
<view class="bottom-button-zaping">
<uv-button
type="primary"
:text="btnConfig.text"
:loading="btnConfig.loading"
:disabled="btnConfig.disabled"
:loading-text="btnConfig.loadingText"
@click="submit"
/>
</view>
</template>
</z-paging>
</template>
<style lang="scss" scoped>
.container {
width: calc(100% - 60rpx);
padding: 10rpx 30rpx;
.describe_tips {
padding: 15rpx 30rpx;
background-color: #effbeb;
margin-top: 20rpx;
border-radius: 10rpx;
font-size: 26rpx;
color: #656f63;
}
.problemTypes {
margin-top: 20rpx;
.type_Bar {
display: flex;
flex-wrap: wrap;
margin-bottom: 15rpx;
.type_item {
padding: 15rpx;
border: 2rpx solid #e5e5e5;
font-size: 26rpx;
color: #646464;
margin: 25rpx 30rpx 0 0;
border-radius: 15rpx;
}
.type_item_active {
border-color: #1488f5 !important;
background-color: #1488f5 !important;
color: white !important;
}
}
}
.titleup {
font-size: 30rpx;
&::before {
content: '';
display: inline-block;
width: 4px; /* 竖线宽度,可调整 */
height: 16px; /* 竖线高度,建议和标题文字高度匹配 */
margin-right: 8px; /* 竖线和标题的间距 */
background: linear-gradient(to bottom, #1488f5, #c0e3f4); /* 渐变颜色,和你之前的背景呼应 */
vertical-align: middle; /* 确保竖线和文字垂直居中对齐 */
position: relative;
top: -1px;
}
}
.fault_bar {
margin-top: 40rpx;
.list_grid {
margin-top: 20rpx;
padding-top: 5rpx;
display: grid;
overflow-y: auto;
grid-template-columns: repeat(4, 1fr); /* 固定5列 */
grid-template-rows: auto auto; /* 两行 */
gap: 16px; /* 项目之间的间距 */
}
.fault_item {
display: flex;
flex-direction: column;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
.icon {
width: 60px;
height: 60px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
}
.text {
text-align: center;
font-size: 12px;
text-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
margin-top: 5px;
}
}
.no-margin-right {
margin-right: 0;
}
}
.oher_question {
margin-top: 40rpx;
}
.upload-img {
margin-top: 40rpx;
margin-bottom: 100px;
}
}
</style>

View File

@ -46,6 +46,14 @@ const menuItems = ref([
icon: 'diyabiao',
size: 18,
},
// {
// key: 'returnFee',
// title: '退',
// isLink: true,
// icon: 'shenqingtuikuan',
// size: 18,
// url: '/pages-sub/refundFee/refundFee',
// },
{
key: 'help',
title: '帮助中心',

View File

@ -14,6 +14,7 @@ store.use(
export default store
export * from './order'
export * from './system'
// 模块统一导出
export * from './token'
export * from './user'

61
src/store/system.ts Normal file
View File

@ -0,0 +1,61 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getDictsAndValuesAPI } from '@/api/system'
export const useSystemStore = defineStore(
'system',
() => {
// 字典数据和系统配置项
const dictsAndValues = ref<any>([])
// 设置字典数据和系统配置项
const setDicsValue = async () => {
try {
// 获取字典数据和系统配置项
const res = await getDictsAndValuesAPI()
if (res) {
dictsAndValues.value = res
}
}
catch (err) {
console.error('字典值异常', err)
}
}
// 获取指定key的字典值
const getDictValue = async (key: string) => {
const dict = dictsAndValues.value.find((item: any) => item.dicCode === key)
return dict?.values || []
// if (dictsAndValues.value.length === 0) {
// try {
// // 获取字典数据和系统配置项
// const res = await getDictsAndValuesAPI()
// if (res) {
// dictsAndValues.value = res
// console.log('字典值', dictsAndValues.value)
// const dict = dictsAndValues.value.find((item: any) => item.dicCode === key)
// return dict?.values || []
// }
// }
// catch (err) {
// console.error('字典值异常', err)
// }
// }
// else {
// const dict = dictsAndValues.value.find((item: any) => item.dicCode === key)
// return dict?.values || []
// }
}
return {
dictsAndValues,
setDicsValue,
getDictValue,
}
},
{
persist: true,
},
)

File diff suppressed because one or more lines are too long