初始化=商城+金融,用于演示.

This commit is contained in:
2025-11-28 16:43:16 +08:00
commit 08580b07ad
117 changed files with 20012 additions and 0 deletions

54
src/hooks/useRequest.ts Normal file
View File

@@ -0,0 +1,54 @@
import type { Ref } from 'vue'
import { ref } from 'vue'
interface IUseRequestOptions<T> {
/** 是否立即执行 */
immediate?: boolean
/** 初始化数据 */
initialData?: T
}
interface IUseRequestReturn<T, P = undefined> {
loading: Ref<boolean>
error: Ref<boolean | Error>
data: Ref<T | undefined>
run: (args?: P) => Promise<T | undefined>
}
/**
* useRequest是一个定制化的请求钩子用于处理异步请求和响应。
* @param func 一个执行异步请求的函数返回一个包含响应数据的Promise。
* @param options 包含请求选项的对象 {immediate, initialData}。
* @param options.immediate 是否立即执行请求默认为false。
* @param options.initialData 初始化数据默认为undefined。
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
*/
export default function useRequest<T, P = undefined>(
func: (args?: P) => Promise<T>,
options: IUseRequestOptions<T> = { immediate: false },
): IUseRequestReturn<T, P> {
const loading = ref(false)
const error = ref(false)
const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
const run = async (args?: P) => {
loading.value = true
return func(args)
.then((res) => {
data.value = res
error.value = false
return data.value
})
.catch((err) => {
error.value = err
throw err
})
.finally(() => {
loading.value = false
})
}
if (options.immediate) {
(run as (args: P) => Promise<T | undefined>)({} as P)
}
return { loading, error, data, run }
}

116
src/hooks/useScroll.md Normal file
View File

@@ -0,0 +1,116 @@
# 上拉刷新和下拉加载更多
在 unibest 框架中,我们通过组合 `useScroll` Hook 可结合 `scroll-view` 组件来轻松实现上拉刷新和下拉加载更多的功能。
场景一 页面滚动
```
definePage({
style: {
navigationBarTitleText: '上拉刷新和下拉加载更多',
enablePullDownRefresh: true,
onReachBottomDistance: 100,
},
})
```
场景二 局部滚动 结合 `scroll-view`
## 关键文件
- `src/hooks/useScroll.ts`: 提供了核心的滚动逻辑处理 Hook。
- `src/pages-sub/demo/scroll.vue`: 一个具体的实现示例页面。
## `useScroll` Hook
`useScroll` 是一个 Vue Composition API Hook它封装了处理下拉刷新和上拉加载的通用逻辑。
### 主要功能
- **管理加载状态**: 自动处理 `loading`(加载中)、`finished`(已加载全部)和 `error`(加载失败)等状态。
- **分页逻辑**: 内部维护分页参数(页码 `page` 和每页数量 `pageSize`)。
- **事件处理**: 提供 `onScrollToLower`(滚动到底部)、`onRefresherRefresh`(下拉刷新)等方法,用于在视图层触发。
- **数据合并**: 自动将新加载的数据追加到现有列表 `list` 中。
### 使用方法
```typescript
import { useScroll } from '@/hooks/useScroll'
import { getList } from '@/service/list' // 你的数据请求API
const {
list, // 响应式的数据列表
loading, // 是否加载中
finished, // 是否已全部加载
error, // 是否加载失败
onScrollToLower, // 滚动到底部时触发的事件
onRefresherRefresh, // 下拉刷新时触发的事件
} = useScroll(getList) // 将获取数据的API函数传入
```
## `scroll-view` 组件
`scroll-view` 是 uni-app 提供的可滚动视图区域组件,它提供了一系列属性来支持下拉刷新和上拉加载。
### 关键属性
- `scroll-y`: 允许纵向滚动。
- `refresher-enabled`: 启用下拉刷新。
- `refresher-triggered`: 控制下拉刷新动画的显示与隐藏,通过 `loading` 状态绑定。
- `@scrolltolower`: 滚动到底部时触发的事件,绑定 `onScrollToLower` 方法。
- `@refresherrefresh`: 触发下拉刷新时触发的事件,绑定 `onRefresherRefresh` 方法。
## 示例代码
以下是 `src/pages-sub/demo/scroll.vue` 中的核心代码,展示了如何将 `useScroll``scroll-view` 结合使用。
```vue
<template>
<view class="scroll-page">
<scroll-view
class="scroll-view"
scroll-y
:refresher-enabled="true"
:refresher-triggered="loading"
@scrolltolower="onScrollToLower"
@refresherrefresh="onRefresherRefresh"
>
<view v-for="item in list" :key="item.id" class="scroll-item">
{{ item.name }}
</view>
<!-- 加载状态提示 -->
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-if="finished" class="finished-tip">没有更多了</view>
<view v-if="error" class="error-tip">加载失败请重试</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { useScroll } from '@/hooks/useScroll'
import { getList } from '@/service/list'
const { list, loading, finished, error, onScrollToLower, onRefresherRefresh } = useScroll(getList)
</script>
<style scoped>
/* 样式省略 */
.scroll-page, .scroll-view {
height: 100%;
}
</style>
```
## 实现步骤总结
1. **创建API**: 确保你有一个返回分页数据的API请求函数例如 `getList`),它应该接受页码和页面大小作为参数。
2. **调用 `useScroll`**: 在你的页面脚本中,导入并调用 `useScroll` Hook将你的API函数作为参数传入。
3. **模板绑定**:
- 使用 `scroll-view` 组件作为滚动容器。
- 将其 `refresher-triggered` 属性绑定到 `useScroll` 返回的 `loading` 状态。
- 将其 `@scrolltolower` 事件绑定到 `onScrollToLower` 方法。
- 将其 `@refresherrefresh` 事件绑定到 `onRefresherRefresh` 方法。
4. **渲染列表**: 使用 `v-for` 指令渲染 `useScroll` 返回的 `list` 数组。
5. **添加加载提示**: 根据 `loading`, `finished`, `error` 状态,在列表底部显示不同的提示信息,提升用户体验。
通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。

74
src/hooks/useScroll.ts Normal file
View File

@@ -0,0 +1,74 @@
import type { Ref } from 'vue'
import { onMounted, ref } from 'vue'
interface UseScrollOptions<T> {
fetchData: (page: number, pageSize: number) => Promise<T[]>
pageSize?: number
}
interface UseScrollReturn<T> {
list: Ref<T[]>
loading: Ref<boolean>
finished: Ref<boolean>
error: Ref<any>
refresh: () => Promise<void>
loadMore: () => Promise<void>
}
export function useScroll<T>({
fetchData,
pageSize = 10,
}: UseScrollOptions<T>): UseScrollReturn<T> {
const list = ref<T[]>([]) as Ref<T[]>
const loading = ref(false)
const finished = ref(false)
const error = ref<any>(null)
const page = ref(1)
const loadData = async () => {
if (loading.value || finished.value)
return
loading.value = true
error.value = null
try {
const data = await fetchData(page.value, pageSize)
if (data.length < pageSize) {
finished.value = true
}
list.value.push(...data)
page.value++
}
catch (err) {
error.value = err
}
finally {
loading.value = false
}
}
const refresh = async () => {
page.value = 1
finished.value = false
list.value = []
await loadData()
}
const loadMore = async () => {
await loadData()
}
onMounted(() => {
refresh()
})
return {
list,
loading,
finished,
error,
refresh,
loadMore,
}
}

171
src/hooks/useUpload.ts Normal file
View File

@@ -0,0 +1,171 @@
import { ref } from 'vue'
import { getEnvBaseUrl } from '@/utils/index'
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/upload`
type TfileType = 'image' | 'file'
type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
interface TOptions<T extends TfileType> {
formData?: Record<string, any>
maxSize?: number
accept?: T extends 'image' ? TImage[] : TFile[]
fileType?: T
success?: (params: any) => void
error?: (err: any) => void
}
export default function useUpload<T extends TfileType>(options: TOptions<T> = {} as TOptions<T>) {
const {
formData = {},
maxSize = 5 * 1024 * 1024,
accept = ['*'],
fileType = 'image',
success,
error: onError,
} = options
const loading = ref(false)
const error = ref<Error | null>(null)
const data = ref<any>(null)
const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
if (size > maxSize) {
uni.showToast({
title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
icon: 'none',
})
return
}
// const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
// const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
// if (!isTypeValid) {
// uni.showToast({
// title: `仅支持 ${accept.join(', ')} 格式的文件`,
// icon: 'none',
// })
// return
// }
loading.value = true
uploadFile({
tempFilePath,
formData,
onSuccess: (res) => {
// 修改这里的解析逻辑,适应不同平台的返回格式
let parsedData = res
try {
// 尝试解析为JSON
const jsonData = JSON.parse(res)
// 检查是否包含data字段
parsedData = jsonData.data || jsonData
}
catch (e) {
// 如果解析失败,使用原始数据
console.log('Response is not JSON, using raw data:', res)
}
data.value = parsedData
// console.log('上传成功', res)
success?.(parsedData)
},
onError: (err) => {
error.value = err
onError?.(err)
},
onComplete: () => {
loading.value = false
},
})
}
const run = () => {
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
// 微信小程序在2023年10月17日之后使用本API需要配置隐私协议
const chooseFileOptions = {
count: 1,
success: (res: any) => {
console.log('File selected successfully:', res)
// 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
// h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
// h5的File有以下字段{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
// App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
// App的File有以下字段{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
let tempFilePath = ''
let size = 0
// #ifdef MP-WEIXIN
tempFilePath = res.tempFiles[0].tempFilePath
size = res.tempFiles[0].size
// #endif
// #ifndef MP-WEIXIN
tempFilePath = res.tempFilePaths[0]
size = res.tempFiles[0].size
// #endif
handleFileChoose({ tempFilePath, size })
},
fail: (err: any) => {
console.error('File selection failed:', err)
error.value = err
onError?.(err)
},
}
if (fileType === 'image') {
// #ifdef MP-WEIXIN
uni.chooseMedia({
...chooseFileOptions,
mediaType: ['image'],
})
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage(chooseFileOptions)
// #endif
}
else {
uni.chooseFile({
...chooseFileOptions,
type: 'all',
})
}
}
return { loading, error, data, run }
}
async function uploadFile({
tempFilePath,
formData,
onSuccess,
onError,
onComplete,
}: {
tempFilePath: string
formData: Record<string, any>
onSuccess: (data: any) => void
onError: (err: any) => void
onComplete: () => void
}) {
uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
filePath: tempFilePath,
name: 'file',
formData,
success: (uploadFileRes) => {
try {
const data = uploadFileRes.data
onSuccess(data)
}
catch (err) {
onError(err)
}
},
fail: (err) => {
console.error('Upload failed:', err)
onError(err)
},
complete: onComplete,
})
}