🎉 init project

This commit is contained in:
2025-08-17 08:50:43 +08:00
parent e3e205773a
commit 829c348370
77 changed files with 31902 additions and 10 deletions

15
.gitignore vendored
View File

@@ -1,11 +1,8 @@
# ---> Vue
# gitignore template for Vue.js projects
#
### Vuejs template
# Recommended template: Node.gitignore
# TODO: where does this rule come from?
docs/_book
# TODO: where does this rule come from?
test/
node_modules/
dist/
npm-debug.log
yarn-error.log
.idea

111
README.md
View File

@@ -1,2 +1,111 @@
# menu-mini
## 项目介绍
- 这是一个基于uni-app vue3 cli的模板项目,主要是集成了常用的css原子化,eslint的代码规则,配置了husky的提交校验,
- uview-plus UI框架,并且对于小程序进行了常用的界面的封装,ios下的安全区域,滚动条等都已经配置好,只需要关心业务内容即可
- 支持换肤功能,非常的干净简洁,没有过多的内容
## 项目启动
* 安装
```html
npm install
```
* 初始化git husky的校验,git commit内容的规则可查看commitlint.config.js文件
```html
npx husky-init
<!-- 初始化之后,需要手动修改.husky/pre-commit 把run test删除-->
```
* 启动开发环境H5
```html
pnpm run dev:h5
```
* 运行到小程序
```html
pnpm run dev:mp-weixin
```
* 构建发布小程序
```html
pnpm run build:mp-weixin-develop
后面的develop只是区别不同的环境,test环境就使用test即可,具体可看package.json
```
## 项目结构
- api 封装了http请求,在其子目录modules下放具体的api请求
- components uni-app自带的easycom目录,以目录名称-文件名称这两者相同,可以直接在界面当中该组件,不需要引用
- pages 界面文件
- hooks 存在自定义的hooks文件
- static 静态文件,因为是小程序项目,图片尽量全部都放在云端,减少体积
- store pinia全局管理的存放文件
- utils 工具类,工具函数存放
- styles 全局的样式文件
- themes 主题文件,在设置之后需要再App.vue的style标签中引用才可以生效
## 插件介绍与使用
- uni-ui uni-app官方的多端UI框架,是easycom的自动引用方式,直接在界面中使用即可,无需引用,ui文档 https://uniapp.dcloud.net.cn/component/uniui/uni-ui.html
- uview-plus的支持vue3的UI框架,文档 https://uview-plus.jiangruyi.com/
- z-paging 滚动加载插件,非常好用
- unocss 原子化css插件
- pinia-plugin-unistorage 支持多端的pinia持久化储存插件,在pinia定义store的时候,加上 unistorage: true即可
```html
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
id: 'user',
state: () => {
return {
}
},
unistorage: true, // 加上这一行,就可以实现state中的值持久化存储
getters: {},
actions: {}
})
```
## 开发界面事项
- 常规界面都使用下面的示例
- custom-page 高度都是100%,默认的情况会padding-top手机状态栏的高度,更多用法请查看custom-page组件
- custom-head 顶部的导航栏,默认的情况下跟小程序的自带的右侧药丸一样的高度
- scroll-page 滚动区域,默认情况下会继承外部的高度,并且内容超过该高度的话,会自动滚动,并且去掉了默认的滚动条
```html
<template>
<custom-page>
<custom-head title-text="演示界面"></custom-head>
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
<scroll-page class="content-container">
<!-- scroll-page 是flex布局,会自动获取界面剩余的全部高度,并且内容超过该高度的话,会自动滚动-->
<div class="h-[3000rpx]">我是滚动的区域</div>
</scroll-page>
<view class="bg-red-500 h-[200rpx]">123</view>
</custom-page>
</template>
<script setup></script>
<style lang="scss"></style>
```
- 主题换肤功能
- 在styles/themes文件下可添加你的主题scss文件,建议文件名与最终主题名相同,然后在App.vue的style中引用你的scss文件,类似下面,default-theme就是你主题的名称,通过修改store/system中的commonThemeName字段为你的主题名称,使用了custom-page作为根节点的界面,都会进行对应的主题更改
```html
//主题scss
.default-theme {
@import 'styles/themes/default-theme.scss';
}
```
```html
<script setup>
import useSystemStore from '@/store/system.js'
const systemStore = useSystemStore()
// 换肤
systemStore.setThemeName('blue')
</script>
```
## 封装两个很常用的组件,可以直接下载项目运行查看
- 省市区选择
![](https://cdn.nlark.com/yuque/0/2025/png/2970129/1753425272786-64072c4f-40c2-4512-bd67-9d743298707a.png)
- 上传图片与视频
![](https://cdn.nlark.com/yuque/0/2025/png/2970129/1753429032538-27fd247a-c2de-4e4a-8bab-bec07e4e2acd.png)

70
auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,70 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useModel: typeof import('vue')['useModel']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

23
commitlint.config.cjs Normal file
View File

@@ -0,0 +1,23 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'build', // 编译相关的修改,例如发布版本、对项目构建或者依赖的改动
'feat', // 新功能
'fix', // 修补bug
'docs', // 文档修改
'style', // 代码格式修改
'refactor', // 重构
'perf', // 优化相关,比如提升性能、体验
'test', // 测试用例修改
'revert', // 代码回滚
'ci', // 持续集成修改
'config', // 配置修改
'chore' // 其他改动
]
]
}
}

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

28
jsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "esnext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"types": ["@dcloudio/types", "@uni-helper/uni-app-types", "miniprogram-api-typings", "uview-plus/types", "z-paging/types"],
"allowJs": true,
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"include": ["src", "types"]
},
"vueCompilerOptions": {
"plugins": ["@uni-helper/uni-app-types/volar-plugin"]
},
"exclude": ["dist", "node_modules", "uni_modules"]
}

14347
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

121
package.json Normal file
View File

@@ -0,0 +1,121 @@
{
"id": "zy-vue3-uniapp",
"name": "uni-preset-vue",
"displayName": "zy-vue3-uniapp",
"version": "0.0.0",
"description": "这是一个基于uni-app vue3 cli的模板项目,主要是集成了常用的css原子化,eslint的代码规则,配置了husky的提交校验, uview-plus UI框架,",
"keywords": [
"vue3",
"小程序",
"H5",
"模板"
],
"dcloudext": {
"type": "pagetemplate-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
}
},
"uni_modules": {
"dependencies": [],
"encrypt": []
},
"type": "module",
"scripts": {
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:mp-xhs": "uni -p mp-xhs",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:mp-xhs": "uni build -p mp-xhs",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"prepare": "husky install"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4070520250711001",
"@dcloudio/uni-app-harmony": "3.0.0-4070520250711001",
"@dcloudio/uni-app-plus": "3.0.0-4070520250711001",
"@dcloudio/uni-components": "3.0.0-4070520250711001",
"@dcloudio/uni-h5": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-alipay": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-baidu": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-harmony": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-jd": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-lark": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-qq": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-weixin": "3.0.0-4070520250711001",
"@dcloudio/uni-mp-xhs": "3.0.0-4070520250711001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4070520250711001",
"@uni-helper/vite-plugin-uni-tailwind": "^0.14.2",
"@uni-ui/code-ui": "^1.5.3",
"await-to-js": "^3.0.0",
"clipboard": "^2.0.11",
"dayjs": "^1.11.13",
"image-tools": "^1.4.0",
"luch-request": "^3.1.1",
"uview-plus": "^3.4.47",
"vue": "3.5.18",
"vue-i18n": "9.14.5",
"z-paging": "^2.7.5"
},
"devDependencies": {
"@commitlint/cli": "^17.8.0",
"@commitlint/config-conventional": "^17.8.0",
"@dcloudio/types": "3.4.19",
"@dcloudio/uni-automator": "3.0.0-4070520250711001",
"@dcloudio/uni-cli-shared": "3.0.0-4070520250711001",
"@dcloudio/uni-stacktracey": "3.0.0-4070520250711001",
"@dcloudio/vite-plugin-uni": "3.0.0-4070520250711001",
"@rushstack/eslint-patch": "^1.3.3",
"@unocss/eslint-plugin": "^0.63.6",
"@unocss/preset-icons": "^0.63.6",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/runtime-core": "3.5.18",
"autoprefixer": "^10.4.16",
"eslint": "^8.49.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.17.0",
"husky": "^8.0.0",
"pinia": "2.0.36",
"pinia-plugin-unistorage": "^0.0.17",
"postcss": "^8.4.33",
"prettier": "^3.0.3",
"sass": "1.63.2",
"sass-loader": "10.4.1",
"unocss": "0.63.6",
"unocss-preset-weapp": "^66.0.1",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
"vite": "5.2.8"
}
}

10171
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

10
shims-uni.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/// <reference types='@dcloudio/types' />
import 'vue'
declare module '@vue/runtime-core' {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {
}
}

27
src/App.vue Normal file
View File

@@ -0,0 +1,27 @@
<script setup>
import { useSystemStore } from '@/store/system.js'
import { onLaunch } from '@dcloudio/uni-app'
const systemStore = useSystemStore()
onLaunch(() => {
systemStore.init()
})
</script>
<style lang="scss">
@import 'uview-plus/index.scss';
/*每个页面公共css */
@import 'styles/global.scss';
@import 'static/font/iconfont.css';
//主题scss
.default-theme {
@import 'styles/themes/default-theme.scss';
}
//主题scss
.red-theme {
@import 'styles/themes/red-theme.scss';
}
</style>
<style></style>

111
src/api/http.js Normal file
View File

@@ -0,0 +1,111 @@
import Request from 'luch-request'
const tokeyKey = 'Authorization'
const env = import.meta.env
// 创建实例
const axiosInstance = new Request({
baseURL: env.VITE_BASE_URL,
timeout: 30 * 1000, // 超时配置
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截
axiosInstance.interceptors.request.use(
config => {
const token = ''
if (token) {
config.headers[tokeyKey] = ''
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截
axiosInstance.interceptors.response.use(
response => {
// console.log('response=>', response)
const { status, data, config } = response
if (status === 200) {
const { code, message, data: resData } = data
if (code === '0000') {
return resData
}
// 文件
if (config?.responseType === 'blob') {
return response
}
// token过期
if (code === '5010') {
uni.showToast({
title: 'token过期请重新登陆',
icon: 'none'
})
// userStore.logout(false)
return
}
uni.showToast({
title: message || '接口请求异常',
icon: 'none'
})
return Promise.reject(data)
}
uni.showToast({
title: `${status}服务器响应异常`,
icon: 'none'
})
return Promise.reject(data)
},
error => {
// 如果是取消请求返回空的Promise避免触发异常处理
if (axiosInstance.isCancel(error)) {
return new Promise(() => {})
}
const { response, code, message, config } = error
if (!response) {
uni.showToast({
title: '连接超时,请稍后重试',
icon: 'none'
})
} else {
const { _retry } = config
const { status } = response
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1 && !_retry) {
uni.showToast({
title: '连接超时,请稍后重试',
icon: 'none'
})
} else {
uni.showToast({
title: `${status}`,
icon: 'none'
})
}
}
return Promise.reject(error)
}
)
/**
* 上传文件
* @param {*} url
* @param {*} data
* @returns
*/
export const upload = (url, data) => {
let formData = new FormData()
Object.keys(data || []).forEach(key => {
formData.append(key, data[key])
})
return axiosInstance.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export default axiosInstance

12
src/api/modules/test.js Normal file
View File

@@ -0,0 +1,12 @@
import http from '@/api/http.js'
const VITE_BASE_URL_AUTH = import.meta.env.VITE_BASE_URL_AUTH // 认证中心
export default {
login: data => {
return http.request({
baseURL: VITE_BASE_URL_AUTH,
url: '/login',
method: 'post',
data
})
}
}

View File

@@ -0,0 +1,17 @@
<template>
<view class="flex flex-col items-center">
<image class="w-[133rpx] h-[133rpx] mt-[93rpx]" src="@/static/images/loading.gif"></image>
<text class="color-text-main font-bold text-[36rpx] mt-[40rpx]">正在查询支付结果</text>
</view>
</template>
<script setup>
const emit = defineEmits(['result'])
setTimeout(() => {
emit('result', 4)
}, 2000)
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,98 @@
<template>
<view>
<view class="warning-info">
<i
class="iconfont icon-a-Lineartishi text-[32rpx] mr-[19rpx]"
style="color: var(--color-function-warning)"
></i>
<view class="text-[26rpx]" style="color: var(--color-function-warning)">
请使用张清橙*8888的银行账户向以下账户自行转账为确保订单正常发货转账金额需保持一致
</view>
</view>
<view class="form-box">
<view class="row-item" v-for="(item, index) in formItems" :key="index">
<text class="item-label">{{ item.label }}</text>
<view class="flex items-center">
<text class="mr-[15rpx]" :style="item.style">{{ item.showText }}</text>
<view class="copy-item" @click="copy(item.value)">复制</view>
</view>
</view>
<view class="primary-button-box h-[96rpx] mt-[44rpx]" @click="confirmHandle">
我已完成转账
</view>
</view>
</view>
</template>
<script setup>
const formItems = ref([
{
label: '转账金额',
showText: '¥120,999.00',
style: 'fontWeight:bold',
value: ''
},
{
label: '收款公司名称',
showText: '江楠鲜品科技有限公司',
value: ''
},
{
label: '收款账户',
showText: '8888 8888 8888 8888',
value: ''
},
{
label: '开户行名称',
showText: '招商银行广州白云支行',
value: ''
}
])
const copy = value => {
uni.setClipboardData({
data: value
})
}
const emits = defineEmits(['success'])
const confirmHandle = () => {
emits('success')
}
</script>
<style scoped lang="scss">
.form-box {
padding: 0 24rpx;
}
.warning-info {
height: 112rpx;
background: #fff7f2;
display: flex;
align-items: center;
padding: 0 28rpx;
}
.row-item {
height: 96rpx;
margin: 0 24rpx;
border-bottom: 1rpx solid var(--color-division-line);
display: flex;
justify-content: space-between;
align-items: center;
.item-label {
font-size: 32rpx;
color: var(--color-text-main);
}
}
.copy-item {
width: 91rpx;
text-align: center;
border-left: 1rpx solid var(--color-division-line);
font-size: 26rpx;
color: var(--color-text-secondary);
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<view class="flex flex-col items-center justify-between pl-[24rpx] pr-[24rpx] w-full h-full">
<view class="flex flex-col items-center">
<i
class="text-[133rpx] mt-[93rpx] iconfont icon-a-Fillcuowu"
style="color: var(--color-function-error); line-height: 1"
></i>
<text class="color-text-main font-bold text-[36rpx] mt-[40rpx]">暂未收到转账信息</text>
<text class="fail-hint">
我们正在处理您的转账由于系统延迟目前尚未确认确认到来自张清橙
(*8888)的转账我们会持续更新进度请稍后查看感谢您的理解与支持
</text>
</view>
<view class="primary-button-box h-[96rpx] mt-[44rpx] mb-[24rpx] w-full" @click="confirmHandle">
重新转账
</view>
</view>
</template>
<script setup>
const emits = defineEmits(['result'])
const confirmHandle = () => {
emits('result', 1)
}
</script>
<style scoped lang="scss">
.fail-hint {
font-size: 28rpx;
color: var(--color-text-secondary);
margin-top: 8rpx;
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<view class="flex flex-col items-center justify-between pl-[24rpx] pr-[24rpx] w-full h-full">
<view class="flex flex-col items-center">
<i
class="text-[133rpx] mt-[93rpx] iconfont icon-a-Fillchenggong"
style="color: var(--color-primary); line-height: 1"
></i>
<text class="color-text-main font-bold text-[36rpx] mt-[40rpx]">转账成功</text>
</view>
<view class="primary-button-box h-[96rpx] mt-[44rpx] w-full mb-[24rpx]" @click="success">
完成
</view>
</view>
</template>
<script setup>
const success = () => {}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,54 @@
<template>
<!-- 转账弹窗-->
<cf-bottom-dialog ref="dialogRef" type="bottom">
<view>
<view class="content-box">
<!-- 转账的信息内容-->
<transferContent v-if="form.state == 1" @success="changeState(2)"></transferContent>
<!-- 查询-->
<loading v-if="form.state == 2" @result="changeState"></loading>
<!-- 转账成功-->
<transferSuccess v-if="form.state == 3"></transferSuccess>
<!-- 转账失败-->
<transferFail v-if="form.state == 4" @result="changeState"></transferFail>
</view>
</view>
</cf-bottom-dialog>
</template>
<script setup name="TransferAccount">
import transferContent from './components/transfer-content.vue'
import loading from './components/loading.vue'
import transferSuccess from './components/transfer-success.vue'
import transferFail from './components/transfer-fail.vue'
const dialogRef = ref()
const form = ref({
state: 1
})
const changeState = state => {
dialogRef.value.open('转账结果')
form.value.state = state
}
const confrimHanle = () => {
uni.navigateTo({
url: '/pages/payResult/payResult?category=success'
})
}
const open = () => {
dialogRef.value.open('请转账到')
}
defineExpose({
open
})
</script>
<style scoped lang="scss">
.content-box {
height: 680rpx;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<!-- 验证码弹窗-->
<cf-middle-dialog ref="dialogRef">
<view>
<view class="pay-name color-text-main">{{ form.payName }}</view>
<view class="font-bold text-center mt-[8rpx]">
<text class="text-[48rpx]"></text>
<text class="text-[64rpx]">{{ form.price }}</text>
</view>
<view class="code-form-box">
<uni-easyinput
type="number"
v-model="form.code"
:inputBorder="false"
placeholder="请输入验证码"
:placeholderStyle="placeholderStyle"
primaryColor="var(--color-text-disable)"
></uni-easyinput>
<view class="count-down">
{{ smsCodeBtnText }}
</view>
</view>
<view class="text-center color-text-secondary text-[28rpx] mt-[32rpx]">
验证码已发送至: 132****8206
</view>
<view class="primary-button-box h-[92rpx] mt-[32rpx]" @click="confrimHanle">确认</view>
</view>
</cf-middle-dialog>
</template>
<script setup name="verificationCode">
import { useSmsCodeCountDown } from '@/hooks/useGlobalUtil'
const { smsCodeBtnText, isCountDown, startCountDownInterval } = useSmsCodeCountDown()
const dialogRef = ref()
const placeholderStyle = 'font-size:32rpx;color:var(--color-text-disable)'
const form = ref({
payName: '钱包支付',
price: '1299.00',
code: ''
})
const confrimHanle = () => {
uni.navigateTo({
url: '/pages/payResult/payResult?category=success'
})
}
const open = () => {
dialogRef.value.open('请输入验证码')
startCountDownInterval()
}
defineExpose({
open
})
</script>
<style scoped lang="scss">
.pay-name {
margin-top: 31rpx;
text-align: center;
font-size: 28rpx;
}
.code-form-box {
height: 96rpx;
border-top: 1rpx solid var(--color-division-line);
border-bottom: 1rpx solid var(--color-division-line);
margin-top: 64rpx;
display: flex;
align-items: center;
}
.count-down {
width: 194rpx;
height: 48rpx;
font-size: 32rpx;
color: var(--color-text-secondary);
line-height: 48rpx;
text-align: right;
border-left: 1rpx solid var(--color-division-line);
}
</style>

View File

@@ -0,0 +1,36 @@
# 省市区选择
> 省市区街道选择组件,不依赖任何UI框架,直接拷贝也可以使用(**注意JSON文件在小程序中,建议最好放到云端**)
>
![](https://cdn.nlark.com/yuque/0/2025/png/2970129/1753425272786-64072c4f-40c2-4512-bd67-9d743298707a.png)
### 使用
```vue
<city-picker
:visible="cityPickerVisible"
@cancel="cityPickerVisible = false">
</city-picker>
```
### 属性说明props
| **属性名** | **说明** | **类型** | **默认值** | **可选值** |
| --- | --- | --- | --- | --- |
| column | 显示的列数,如果是3的话,那就只有省市区 | Number | 4 | 1|2|3|4 |
| defaultValue | 传入中文的省市区街道数组,会自动回显 | Array | [] | - |
| visible | 是否显示组件 | Boolean | false | true |
| maskCloseAble | 点击蒙版是否关闭 | Boolean | true | false |
### 回调事件
| **事件名** | **说明** | **回调参数** |
| --- | --- | --- |
| cancel | 点击取消按钮回调事件 | <font style="color:rgb(44, 62, 80);"></font> |
| confirm | 点击确认回调事件 | {<br/>code:[], // 选择地址的省市区<br/>name: '', // 选择地址的名称拼接好的字符串<br/>provinceName:'', // 省名称<br/>cityName:'', // 市名称<br/>areaName:'', // 区名称<br/>streetName:'', // 街道名称<br/>} |

View File

@@ -0,0 +1,270 @@
<template>
<view class="pupop">
<view class="popup-box" :animation="animationData">
<view class="pupop-btn">
<view @tap="cancel">取消</view>
<view @tap="confirm" style="color: var(--color-function-success)">确定</view>
</view>
<picker-view
v-if="addressList.length"
:value="value"
:indicator-style="indicatorStyle"
@change="onChange"
class="picker-view"
>
<picker-view-column>
<view class="item" v-for="(item, index) in provinceList" :key="index">
{{ item.name }}
</view>
</picker-view-column>
<picker-view-column>
<view class="item" v-for="(item, index) in cityList" :key="index">{{ item.name }}</view>
</picker-view-column>
<picker-view-column>
<view class="item" v-for="(item, index) in areaList" :key="index">{{ item.name }}</view>
</picker-view-column>
<picker-view-column v-if="column === 4">
<view class="item" v-for="(item, index) in streetList" :key="index">{{ item.name }}</view>
</picker-view-column>
</picker-view>
</view>
<view
@tap="close"
@touchmove.stop.prevent
:class="visible ? 'pupop-model' : 'pupop-models'"
></view>
</view>
</template>
<script setup>
import { ref, watch, nextTick, computed } from 'vue'
import addressData from './pcas-code.json'
// import { addressList as addressData } from './cityData.js'
let addressList = reactive([])
const getAddressList = async () => {
// const { data } = await addressApi.getPcasJson()
// 这里建议放到项目中时,把地图的json数据放到云端,避免小程序主包过大
addressList = addressData
init()
}
onMounted(() => {
getAddressList()
})
const props = defineProps({
column: {
// 显示的列数如果是3的话就只有省市区没有街道以此类推
type: Number,
default: 4
},
defaultValue: {
// 传入中文的省市区街道数组,则会自动回显
type: [String, Array],
default: ''
},
visible: {
type: Boolean,
default: false
},
maskCloseAble: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['confirm', 'cancel'])
const value = ref([])
const animationData = ref({})
const indicatorStyle = 'height: 50px;'
const provinceList = ref([])
const cityList = ref([])
const areaList = ref([])
const streetList = ref([])
const provinceIndex = ref(0)
const cityIndex = ref(0)
const areaIndex = ref(0)
const streetIndex = ref(0)
watch(
() => props.visible,
() => {
triggerAnimation()
}
)
watch(
() => props.defaultValue,
() => {
init()
}
)
function init() {
if (!addressList.length) return
provinceList.value = addressList.map(item => ({
name: item.name,
code: item.code
}))
console.log('provinceList.value -=---', provinceList.value)
if (!props.defaultValue) {
cityList.value = addressList[0].children
areaList.value = cityList.value[0].children
streetList.value = areaList.value[0]?.children || []
} else if (Array.isArray(props.defaultValue)) {
const [pName, cName, aName, sName] = props.defaultValue
provinceIndex.value = addressList.findIndex(p => p.name === pName)
if (provinceIndex.value < 0) provinceIndex.value = 0
const province = addressList[provinceIndex.value]
cityList.value = province.children || []
cityIndex.value = cityList.value.findIndex(c => c.name === cName)
if (cityIndex.value < 0) cityIndex.value = 0
const city = cityList.value[cityIndex.value]
areaList.value = city.children || []
areaIndex.value = areaList.value.findIndex(a => a.name === aName)
if (areaIndex.value < 0) areaIndex.value = 0
const area = areaList.value[areaIndex.value]
streetList.value = area.children || []
if (props.column === 4) {
streetIndex.value = streetList.value.findIndex(s => s.name === sName)
if (streetIndex.value < 0) streetIndex.value = 0
}
nextTick(() => {
value.value = [provinceIndex.value, cityIndex.value, areaIndex.value]
if (props.column === 4) value.value.push(streetIndex.value)
})
}
triggerAnimation()
}
function triggerAnimation() {
const animation = uni.createAnimation({
duration: 400,
timingFunction: 'linear'
})
animation.bottom(props.visible ? 0 : '-350px').step()
animationData.value = animation.export()
}
function onChange(e) {
if (!addressList.length) return
const v = e.detail.value
const [p = 0, c = 0, a = 0, s = 0] = v
if (p !== provinceIndex.value) {
provinceIndex.value = p
cityIndex.value = 0
areaIndex.value = 0
streetIndex.value = 0
cityList.value = addressList[p].children
areaList.value = cityList.value[0]?.children || []
streetList.value = areaList.value[0]?.children || []
} else if (c !== cityIndex.value) {
cityIndex.value = c
areaIndex.value = 0
streetIndex.value = 0
areaList.value = cityList.value[c]?.children || []
streetList.value = areaList.value[0]?.children || []
} else if (a !== areaIndex.value) {
areaIndex.value = a
streetIndex.value = 0
streetList.value = areaList.value[a]?.children || []
} else if (s !== streetIndex.value && props.column === 4) {
streetIndex.value = s
}
value.value = [provinceIndex.value, cityIndex.value, areaIndex.value]
if (props.column === 4) value.value.push(streetIndex.value)
}
function confirm() {
const province = addressList[provinceIndex.value]
const city = cityList.value[cityIndex.value]
const area = areaList.value[areaIndex.value]
const street = streetList.value[streetIndex.value]
const data = {
code: [province?.code, city?.code, area?.code],
name: province.name + city.name + area.name,
provinceName: province.name,
cityName: city.name,
areaName: area.name
}
if (props.column === 4 && street) {
data.code = [...data.code, street.code]
data.name += street.name
data.streetName = street.name
}
emit('confirm', data)
}
function cancel() {
emit('cancel')
}
function close() {
if (props.maskCloseAble) {
cancel()
}
}
</script>
<style scoped lang="scss">
.pupop {
.popup-box {
position: fixed;
left: 0;
bottom: -315px;
z-index: 99999;
background: #fff;
padding-bottom: 50px;
.pupop-btn {
height: 40px;
display: flex;
align-items: center;
padding: 0 30upx;
justify-content: space-between;
}
}
.pupop-model {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 9999;
background: rgba(0, 0, 0, 0.5);
}
.pupop-models {
display: none;
}
.picker-view {
width: 750rpx;
height: 225px;
margin-top: 20rpx;
}
.item {
height: 50px;
align-items: center;
justify-content: center;
text-align: center;
line-height: 50px;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,70 @@
<template>
<root-portal>
<u-popup
v-bind="$attrs"
mode="bottom"
v-model="show"
:border-radius="24"
:mask-custom-style="{ background: 'var(--color-bg-mask)' }"
>
<view class="popup-content" :style="customStyle">
<view class="dialog-header">
<text>{{ title }}</text>
<i class="iconfont icon-a-Linearguanbi text-[36rpx] close-icon" @tap="close"></i>
</view>
<slot></slot>
</view>
</u-popup>
</root-portal>
</template>
<script setup>
defineProps({
customStyle: {
type: Object,
default: () => {
return {}
}
}
})
const title = ref('提示')
const show = ref(false)
const open = titleStr => {
title.value = titleStr
show.value = true
}
const emits = defineEmits(['close'])
const close = () => {
show.value = false
emits('close')
}
defineExpose({
open,
close
})
</script>
<style scoped lang="scss">
.dialog-header {
text-align: center;
height: 112rpx;
line-height: 112rpx;
color: var(--color-text-main);
font-size: 32rpx;
position: relative;
font-weight: 600;
}
.close-icon {
position: absolute;
right: 34rpx;
top: 0rpx;
color: var(--color-text-disable);
}
.popup-content {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<view class="checkbox-container" :class="checkboxClassHandle" @tap.stop="changeCheckbox">
<i class="iconfont icon-a-Lineargou text-[#fff]" v-if="checkboxSelected"></i>
</view>
<!-- 需要放大的热区,跟父元素一样的大小-->
<div class="scale-box" v-if="showScale" @tap.stop="changeCheckbox"></div>
</template>
<script setup>
const props = defineProps({
modelValue: {
// v-model的值,一般用于只有一个复选框时
type: Boolean
},
selected: {
// 当前组件是否选中状态
type: Boolean
},
disabled: {
type: Boolean,
default: false
},
showScale: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['update:modelValue', 'change', 'customClick'])
const checkboxSelected = computed({
get() {
return props.modelValue || props.selected
},
set(value) {
emits('update:modelValue', value)
emits('change', value)
}
})
const checkboxClassHandle = computed(() => {
let classNames = props.disabled ? 'disabled ' : ''
classNames += checkboxSelected.value ? 'selected' : ''
return classNames
})
const changeCheckbox = () => {
if (props.disabled) return
checkboxSelected.value = !checkboxSelected.value
emits('customClick', checkboxSelected.value)
}
</script>
<style scoped lang="scss">
.checkbox-container {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #ffffff;
border: 2.4rpx solid #dcdcdc;
display: flex;
justify-content: center;
align-items: center;
position: relative;
&.selected {
background: #2e69ff;
border: none;
}
&.disabled {
background: rgba(0, 0, 0, 0.1);
}
}
.scale-box {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<uni-popup ref="popupRef" v-bind="$attrs" mask-background-color="var(--color-bg-mask)">
<view class="popup-content" :style="{ width: `${props.width}rpx` }">
<view class="dialog-header">
<text>{{ title }}</text>
<i class="iconfont icon-a-Linearguanbi text-[36rpx] close-icon" @tap="close"></i>
</view>
<slot></slot>
</view>
</uni-popup>
</template>
<script setup>
const props = defineProps({
width: {
type: Number,
default: 630
}
})
const title = ref('提示')
const popupRef = ref()
const open = titleStr => {
title.value = titleStr
popupRef.value.open()
}
const close = () => {
popupRef.value.close()
}
defineExpose({
open,
close
})
</script>
<style scoped lang="scss">
.dialog-header {
text-align: center;
height: 119rpx;
line-height: 119rpx;
color: var(--color-text-main);
font-size: 36rpx;
position: relative;
font-weight: 500;
border-bottom: 1rpx solid var(--color-division-line);
}
.close-icon {
position: absolute;
right: 0;
top: 0rpx;
color: var(--color-text-disable);
}
.popup-content {
background: #fff;
border-radius: 20rpx;
padding: 0 48rpx 48rpx 48rpx;
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<view class="checkbox-container" :class="checkboxClassHandle" @click="changeCheckbox">
<view class="circle" v-if="checkboxSelected"></view>
</view>
</template>
<script setup>
const props = defineProps({
modelValue: {
// v-model的值,一般用于只有一个复选框时
type: Boolean
},
selected: {
// 当前组件是否选中状态
type: Boolean
},
disabled: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['update:modelValue', 'change'])
const checkboxSelected = computed({
get() {
return props.modelValue || props.selected
},
set(value) {
emits('update:modelValue', value)
emits('change', value)
}
})
const checkboxClassHandle = computed(() => {
let classNames = props.disabled ? 'disabled ' : ''
classNames += checkboxSelected.value ? 'selected' : ''
return classNames
})
const changeCheckbox = () => {
if (props.disabled) return
checkboxSelected.value = !checkboxSelected.value
}
</script>
<style scoped lang="scss">
.checkbox-container {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #ffffff;
border: 2.4rpx solid #dcdcdc;
&.selected {
border-color: var(--color-function-success);
}
.circle {
margin: 8rpx 0 0 8rpx;
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background: var(--color-function-success);
}
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<view class="pupop">
<view class="popup-box" :animation="animationData">
<view class="pupop-btn">
<view @tap="close">取消</view>
<view @tap="confirm" style="color: var(--color-function-success)">确定</view>
</view>
<picker-view
:value="value"
@change="bindChange"
:indicator-style="indicatorStyle"
class="picker-view"
>
<picker-view-column>
<view class="item" v-for="(item, index) in options" :key="index">
{{ item.n }}
</view>
</picker-view-column>
</picker-view>
</view>
<view
@tap="close"
@touchmove.stop.prevent
:class="visible ? 'pupop-model' : 'pupop-models'"
></view>
</view>
</template>
<script setup>
// 街道选择器,用于省市区跟街道分开的选择器
const props = defineProps({
areaCode: {
type: String,
require: true
},
streetData: {
type: Array,
default() {
return []
}
}
})
const value = ref([])
const indicatorStyle = `height: 50px;`
const emits = defineEmits(['confirm', 'cancel'])
const options = computed(() => {
return props.streetData.filter(e => e.areaCode === props.areaCode)
})
const selectedStreet = ref(options.value[0])
const bindChange = e => {
selectedStreet.value = options.value[e.detail.value[0]]
}
const visible = ref(false)
const animationData = ref('')
const confirm = () => {
visible.value = false
changeActive()
emits('confirm', selectedStreet.value)
emitCancel()
}
const changeActive = () => {
let active = '-315px'
if (visible.value) {
active = 0
}
let animation = uni.createAnimation({
duration: 400,
timingFunction: 'linear'
})
animation.bottom(active).step()
animationData.value = animation.export()
}
const emitCancel = () => {
setTimeout(() => {
emits('cancel')
}, 400)
}
// 点击模态框
const close = () => {
visible.value = false
changeActive()
emitCancel()
}
onMounted(() => {
visible.value = true
changeActive()
})
</script>
<style scoped lang="scss">
.pupop {
.popup-box {
position: fixed;
left: 0;
bottom: -315px;
z-index: 99999;
background: #fff;
padding-bottom: 50px;
.pupop-btn {
height: 40px;
display: flex;
align-items: center;
padding: 0 30upx;
justify-content: space-between;
}
}
.pupop-model {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 9999;
background: rgba(0, 0, 0, 0.5);
}
.pupop-models {
display: none;
}
.picker-view {
width: 750rpx;
height: 225px;
margin-top: 20rpx;
}
.item {
height: 50px;
align-items: center;
justify-content: center;
text-align: center;
line-height: 50px;
}
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<up-tabbar style="flex: 0" :value="tabBar.currentValue" :fixed="true" :placeholder="true">
<up-tabbar-item
@click="toSwitchPage(item)"
v-for="item in tabBar.tabBarList"
:key="item.name"
:name="item.name"
:text="item.text"
:icon="item.icon"
></up-tabbar-item>
</up-tabbar>
</template>
<script setup>
import { useTabBarStore } from '@/store/tabbar.js'
import { onShow } from '@dcloudio/uni-app'
const tabBar = useTabBarStore()
onShow(() => {
uni.hideTabBar({
animation: false
})
})
const toSwitchPage = item => {
uni.switchTab({
url: item.pagePath
})
tabBar.setValue(item.name)
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,105 @@
> 支持H5与小程序选择上传图片与视频,并且支持数量限制,图片,视频大小限制,自带图片视频的预览,也兼容了IOS在小程序中的预览
>
![](https://cdn.nlark.com/yuque/0/2025/png/2970129/1753429032538-27fd247a-c2de-4e4a-8bab-bec07e4e2acd.png)
### 使用
```vue
<cu-upload-img-video v-model="fileList"></cu-upload-img-video>
```
### 属性props
| **属性名** | **说明** | **类型** | **默认值** | **可选值** |
| --- | --- | --- | --- | --- |
| maxLength | 最多上传多少个文件 | Number | 1 | |
| videoMaxSize | 视频文件的最大大小,单位为M | Number | 100 | |
| imgMaxSize | 图片文件的最大大小,单位为M | Number | 20 | |
| v-model | 文件的字符串数组 | Array | string[] | |
| onlyShow | 是否仅查看,为true时,不展示上传按钮 | Boolean | false | true |
| onlyImg | 是否只上传图片 | Boolean | false | true |
#### 注意事项,需要自己实现upload.js中的上传逻辑,因为每个人的上传逻辑不太一样,就不做封装了
```vue
export const useUpload = async file => {
function getRandom(num) {
let random = Math.floor(
(Math.random() + Math.floor(Math.random() * 9 + 1)) * Math.pow(10, num - 1)
)
return random
}
const fileUri = `${new Date().getTime()}${getRandom(10)}${file.name || ''}`
const uploadFileFn = async arrayBuffer => {
uni.showLoading({
title: '上传中'
})
uni.hideLoading()
return arrayBuffer
// await http
// .request({
// url: uploadUrl.url,
// method: 'PUT',
// data: arrayBuffer // ArrayBuffer数据
// })
// .catch(err => {
// console.error('上传过程中发生错误', err)
// uni.showToast({
// title: '上传异常',
// icon: 'none'
// })
// uni.hideLoading()
// })
//
// uni.hideLoading()
//
// return fileConfig.fileUrl + fileUri
}
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.showLoading({
title: '读取中'
})
uni.getFileSystemManager().readFile({
filePath: file.path,
success: async res => {
console.log('success=====', res)
const url = await uploadFileFn(res.data)
resolve(url)
},
fail: e => {
console.log('fail----', e)
}
})
// #endif
// #ifdef H5
// const reader = new FileReader()
const url = URL.createObjectURL(file)
resolve(url)
console.log('file---', file)
// reader.readAsArrayBuffer(file)
// reader.onload = async () => {
// const arrayBuffer = reader.result
// const url = await uploadFileFn(arrayBuffer)
// resolve(url)
// }
// #endif
})
}
```

View File

@@ -0,0 +1,352 @@
<template>
<!-- 上传start -->
<view style="display: flex; flex-wrap: wrap">
<view class="update-file">
<!--图片-->
<view v-for="(item, index) in fileList" :key="index" class="mr-[12rpx]">
<view class="upload-box">
<image
class="preview-file"
v-if="item.type == 0"
:src="item.url"
@tap="previewImage(item.url)"
mode="aspectFill"
/>
<video v-else-if="item.type == 1" class="preview-file" :src="item.videoUrl"></video>
<view class="remove-icon" @tap="deleteHandle(index)" v-if="!onlyShow">
<i class="iconfont icon-a-Linearguanbi text-[20rpx]"></i>
</view>
</view>
</view>
<!--按钮-->
<view v-if="VideoOfImagesShow && !onlyShow" @tap="chooseVideoImage" class="upload-btn">
<view class="add-box">
<i class="iconfont icon-a-Linearxiangji text-[50rpx]"></i>
<view class="color-text-disable text-[24rpx]">
<view class="text-center">上传</view>
<view class="text-center">最多{{ maxLength }}</view>
</view>
</view>
</view>
</view>
</view>
<!-- 上传 end -->
</template>
<script setup>
import { useUpload } from './upload'
// 设置初始数据和响应式状态
const sourceType = ref(['拍摄', '相册', '拍摄或相册'])
const sourceTypeIndex = ref(2)
const cameraList = reactive([
{ value: 'back', name: '后置摄像头', checked: 'true' },
{ value: 'front', name: '前置摄像头' }
])
const cameraIndex = ref(0)
const props = defineProps({
maxLength: {
type: Number,
default: 1
},
videoMaxSize: {
// 单位为M
type: Number,
default: 100
},
imgMaxSize: {
// 单位为M
type: Number,
default: 20
},
modelValue: {
type: Array,
default: () => []
},
onlyShow: {
// 是否仅查看
type: Boolean,
default: false
},
onlyImg: {
// 是否只上传图片
type: Boolean,
default: false
}
})
const VideoOfImagesShow = computed(() => {
return fileList.value.length < props.maxLength
})
const emits = defineEmits(['update:modelValue'])
const fileList = computed({
get() {
props.modelValue.map(t => {
if (t.type == 1 && /http/.test(t.url) && !t.videoUrl) {
// 如果是视频流,ios需要先下载下来,才能播放,不能直接播放二进制的视频流
uni.downloadFile({
url: t.url,
success: res => {
t.videoUrl = res.tempFilePath
}
})
}
})
return props.modelValue
},
set(newValue) {
emits('update:modelValue', newValue)
}
})
// 方法
function chooseVideoImage() {
if (props.onlyImg) {
chooseImages()
} else {
uni.showActionSheet({
title: '选择上传类型',
itemList: ['图片', '视频'],
success: res => {
if (res.tapIndex == 0) {
chooseImages()
} else {
chooseVideo()
}
}
})
}
}
function chooseImages() {
uni.chooseImage({
count: props.maxLength - fileList.value.length, // 默认9张
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: res => {
const validRes = res.tempFiles.every(item => {
if (item.size > props.imgMaxSize * 1024 * 1000) {
uni.showToast({
title: `图片大小不能超过${props.imgMaxSize}M`,
icon: 'error'
})
return false
}
return true
})
if (!validRes) return
res.tempFiles.map(async t => {
const url = await useUpload(t)
console.log('url----', url)
fileList.value.push({
type: 0,
url: url
})
})
}
})
}
function chooseVideo() {
uni.chooseVideo({
maxDuration: 60,
count: 9,
camera: cameraList[cameraIndex.value].value,
// sourceType: sourceType.value[sourceTypeIndex.value],
success: async res => {
let videoFile = res.tempFile || res
// #ifdef MP-WEIXIN
videoFile.path = videoFile.tempFilePath
// #endif
if (videoFile.size > props.videoMaxSize * 1024 * 1000) {
uni.showToast({
title: `视频大小不能超过${props.videoMaxSize}M`,
icon: 'error'
})
return
}
const url = await useUpload(videoFile)
console.log('videoFile---', videoFile.path)
fileList.value.push({
type: 1,
url: url,
videoUrl: videoFile.path
})
},
fail: error => {
uni.hideLoading()
uni.showToast({
title: error,
icon: 'none'
})
}
})
}
function previewImage(item) {
console.log('预览图片', item)
uni.previewImage({
current: item,
urls: fileList.value.filter(t => t.type == 0).map(t2 => t2.url)
})
}
function deleteHandle(index) {
uni.showModal({
title: '提示',
content: '是否要删除?',
success: res => {
if (res.confirm) {
fileList.value.splice(index, 1)
}
}
})
}
</script>
<style scoped lang="scss">
// 上传
.update-file {
margin-left: 10rpx;
height: auto;
display: flex;
//justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 5rpx;
.del-icon {
width: 44rpx;
height: 44rpx;
position: absolute;
right: 10rpx;
top: 12rpx;
}
.btn-text {
color: #606266;
}
.preview-file {
width: 160rpx;
height: 160rpx;
border: 1px solid #e0e0e0;
border-radius: 10rpx;
object-fit: contain;
}
.upload-box {
position: relative;
width: 160rpx;
height: 160rpx;
margin: 0 10rpx 20rpx 0;
}
.remove-icon {
position: absolute;
right: 10rpx;
top: 10rpx;
z-index: 100;
width: 30rpx;
height: 30rpx;
background: var(--color-function-error);
border-radius: 50%;
text-align: center;
line-height: 30rpx;
color: #fff;
font-size: 16rpx;
//padding: 5rpx;
}
.upload-btn {
width: 160rpx;
height: 160rpx;
border-radius: 10rpx;
background-color: #f4f5f6;
display: flex;
justify-content: center;
align-items: center;
}
}
.guide-view {
margin-top: 30rpx;
display: flex;
.guide-text {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 20rpx;
.guide-text-price {
padding-bottom: 10rpx;
color: #ff0000;
font-weight: bold;
}
}
}
.service-process {
background-color: #ffffff;
padding: 20rpx;
padding-top: 30rpx;
margin-top: 30rpx;
border-radius: 10rpx;
padding-bottom: 30rpx;
.title {
text-align: center;
margin-bottom: 20rpx;
}
}
.form-view-parent {
border-radius: 20rpx;
background-color: #ffffff;
padding: 0rpx 20rpx;
.form-view {
background-color: #ffffff;
margin-top: 20rpx;
}
.form-view-textarea {
margin-top: 20rpx;
padding: 20rpx 0rpx;
.upload-hint {
margin-top: 10rpx;
margin-bottom: 10rpx;
}
}
}
.bottom-class {
margin-bottom: 60rpx;
}
.bottom-btn-class {
padding-bottom: 1%;
.bottom-hint {
display: flex;
justify-content: center;
padding-bottom: 20rpx;
}
}
.add-box {
width: 160rpx;
height: 160rpx;
background: #f7f8f9;
border: 1rpx dashed #c0c4cc;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,76 @@
import Request from 'luch-request'
const http = new Request()
export const useUpload = async file => {
function getRandom(num) {
let random = Math.floor(
(Math.random() + Math.floor(Math.random() * 9 + 1)) * Math.pow(10, num - 1)
)
return random
}
const fileUri = `${new Date().getTime()}${getRandom(10)}${file.name || ''}`
const uploadFileFn = async arrayBuffer => {
uni.showLoading({
title: '上传中'
})
uni.hideLoading()
return arrayBuffer
// await http
// .request({
// url: uploadUrl.url,
// method: 'PUT',
// data: arrayBuffer // ArrayBuffer数据
// })
// .catch(err => {
// console.error('上传过程中发生错误', err)
// uni.showToast({
// title: '上传异常',
// icon: 'none'
// })
// uni.hideLoading()
// })
//
// uni.hideLoading()
//
// return fileConfig.fileUrl + fileUri
}
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.showLoading({
title: '读取中'
})
uni.getFileSystemManager().readFile({
filePath: file.path,
success: async res => {
console.log('success=====', res)
const url = await uploadFileFn(res.data)
resolve(url)
},
fail: e => {
console.log('fail----', e)
}
})
// #endif
// #ifdef H5
// const reader = new FileReader()
const url = URL.createObjectURL(file)
resolve(url)
console.log('file---', file)
// reader.readAsArrayBuffer(file)
// reader.onload = async () => {
// const arrayBuffer = reader.result
// const url = await uploadFileFn(arrayBuffer)
// resolve(url)
// }
// #endif
})
}

View File

@@ -0,0 +1,218 @@
<template>
<view v-if="dialogVisible" class="page-root">
<!-- :width="width" :height="height" :export_scale="export_scale" -->
<!-- 启用边界检测后长条形的图片滑动会有点问题暂时不启用 -->
<cropper ref="image-cropper" :disableRotate="true" disableCtrl :src="src" @load="loadimage" :resetCut="true"
:cutWidth="cutWidth" :cutHeight="cutHeight" :canvasZoom="canvasZoom">
</cropper>
<view class="page-buttons">
<view v-if="src" class="page-button page-button-submit" hover-class="hover-btn" @click="tapComplete">确定
</view>
<view class="page-button-cancel" hover-class="hover-btn" @click="tapCancel">取消
</view>
</view>
</view>
</template>
<script>
/**
* @author: lihai
* @Description: 裁剪图片的页面
*/
import cropper from "./components/uniapp-nice-cropper/cropper.vue";
// #ifdef H5
/**
* @param {Object} dataurl 将base64转blob对象
*/
function dataURLtoBlob(dataurl) {
// console.log(dataurl)
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n); //8位无符号整数长度1个字节
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
// console.log(JSON.stringify(u8arr));
return new Blob([u8arr], {
type: mime
});
}
/**
* base64图片转成URL
*/
function getBase64URL(pic) {
const blob = dataURLtoBlob(pic)
const blobUrl = URL.createObjectURL(blob);
return blobUrl
}
// #endif
export default {
components: {
cropper
},
data() {
return {
src: null,
cutWidth: 250,
//宽度
cutHeight: 250,
canvasZoom: 1, //最终导出的缩放
// export_scale: 3,
confirmFunc: () => {},
cancelFunc: () => {},
dialogVisible: false
}
},
onLoad: function(options) {
},
methods: {
show(options) {
this.cutHeight = Math.round(options.h / options.w * 680) + "rpx";
this.cutWidth = "680rpx";
// 将导出的图像缩放还原成指定的图像尺寸
// rpx = px / uni.getSystemInfoSync().windowWidth * 750
this.canvasZoom = 750 / uni.getSystemInfoSync().windowWidth * options.w / 680;
this.avatarUrl = options.avatarUrl;
uni.showLoading({
title: '加载中'
}); // 选择图片
if (this.avatarUrl) {
this.src = this.avatarUrl;
} else {
uni.chooseImage({
count: 1,
success: (res) => {
let tempFilePath = res.tempFilePaths[0];
this.src = tempFilePath;
}
});
}
return new Promise((resolve, reject) => {
this.dialogVisible = true;
this.confirmFunc = resolve;
this.cancelFunc = reject;
});
},
loadimage(e) {
// console.log("图片加载完成", e.detail);
uni.hideLoading(); //重置图片角度、缩放、位置
},
/**
* 响应 - 裁剪完成
*/
tapComplete: function(e) {
let self = this;
// new Promise((resolve, reject) => {
// resolve({
// url: this.resultImgPath
// })
// })
this.$refs["image-cropper"].runCrop().then(res => {
return new Promise((resolve, reject) => {
uni.getFileInfo({
filePath: res.url,
success: (res) => {
resolve(res)
},
fail: (err) => {
reject(err)
}
})
}).then(fileInfo => {
console.log('fileInfo', res.url, fileInfo);
// #ifdef H5
this.confirmFunc({
tempFilePath: getBase64URL(res.url),
size: fileInfo.size
});
// #endif
// #ifdef MP
this.confirmFunc({
tempFilePath: res.url,
size: fileInfo.size
});
// #endif
});
}).then(() => {
this.dialogVisible = false;
});
},
tapCancel() {
this.dialogVisible = false;
this.cancelFunc();
}
}
}
</script>
<style scoped>
.page-root {
width: 100vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
z-index: 999;
}
.page-buttons {
position: fixed;
z-index: 1000;
bottom: 0;
height: 208rpx;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
}
.page-button {
width: 400rpx;
height: 88rpx;
background-color: white;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: center;
border: 4rpx #0D889A solid;
border-radius: 110rpx;
font-size: 36rpx;
box-sizing: border-box;
}
.page-button-disable {
opacity: 0.5;
}
.page-button-submit {
color: white;
background-color: #0D889A;
}
.page-button-cancel {
/* background-color: white; */
margin-top: 18rpx;
color: white;
}
</style>

View File

@@ -0,0 +1,571 @@
const ABS = Math.abs;
const calcLen = (v) => {
return Math.sqrt(v.x * v.x + v.y * v.y)
};
const calcAngle = (a, b) => {
var l = calcLen(a) * calcLen(b);
var cosValue;
var angle;
if (l) {
cosValue = (a.x * b.x + a.y * b.y) / l;
angle = Math.acos(Math.min(cosValue, 1));
angle = a.x * b.y - b.x * a.y > 0 ? -angle : angle;
return angle * 180 / Math.PI
}
return 0
};
const generateCanvasId = () => {
const seeds = 'abcdefghijklmnopqrstuvwxyz';
const arr = seeds.split('').concat(seeds.toUpperCase().split('')).concat('0123456789'.split(''));
let m = arr.length;
let i;
while (m) {
i = Math.floor(Math.random() * m--);
const temp = arr[m];
arr[m] = arr[i];
arr[i] = temp
}
return arr.slice(0, 16).join('')
};
export default {
props: {
width: {
type: [String, Number],
default: '100%'
},
height: {
type: [String, Number],
default: '100%'
},
cutWidth: {
type: [String, Number],
default: '50%'
},
cutHeight: {
type: [String, Number],
default: 0
},
minWidth: {
type: Number,
default: 50
},
minHeight: {
type: Number,
default: 50
},
center: {
type: Boolean,
default: true
},
src: String,
disableScale: Boolean,
disableRotate: Boolean,
disableTranslate: Boolean,
disableCtrl: Boolean,
boundDetect: Boolean,
freeBoundDetect: Boolean,
keepRatio: Boolean,
disablePreview: Boolean,
showCtrlBorder: Boolean,
resetCut: Boolean,
fit: {
type: Boolean,
default: true
},
imageCenter: Boolean,
maxZoom: {
type: Number,
default: 10
},
minZoom: {
type: Number,
default: 1
},
angle: {
type: Number,
default: 0
},
zoom: {
type: Number,
default: 1
},
offset: {
type: Array,
default () {
return [0, 0]
}
},
background: {
type: String,
default: '#000'
},
canvasBackground: {
type: String,
default: '#fff'
},
canvasZoom: {
type: Number,
default: 1
},
fileType: {
type: String,
default: 'png',
validator(t) {
return ['png', 'jpg'].includes(t)
}
},
quality: {
type: Number,
default: 1
},
maskType: {
type: String,
default: "shadow"
},
circleView: Boolean
},
data() {
return {
transform: {
angle: 0,
translate: {
x: 0,
y: 0
},
zoom: 1
},
corner: {
left: 50,
right: 50,
bottom: 50,
top: 50
},
image: {
originWidth: 0,
originHeight: 0,
width: 0,
height: 0
},
ctrlWidth: 0,
ctrlHeight: 0,
view: false,
canvasId: ''
}
},
computed: {
transformMeta: function() {
const transform = this.transform;
return `translate3d(${transform.translate.x}px,${transform.translate.y}px,0)rotate(${transform.angle}deg)scale(${transform.zoom})`
},
ctrlStyle: function() {
const corner = this.corner;
let cssStr =
`left:${corner.left}px;top:${corner.top}px;right:${corner.right}px;bottom:${corner.bottom}px;`;
if (this.maskType !== 'outline') {
cssStr += `box-shadow:0 0 0 50000rpx rgba(0,0,0,${this.view?0.8:0.4})`
} else {
cssStr += `outline:rgba(0,0,0,${this.view?0.8:0.4})solid 5000px`
}
return cssStr
}
},
watch: {
src: function() {
if (this.resetCut) this.resetCutReact();
this.initImage()
}
},
created() {
this.canvasId = generateCanvasId();
uni.getSystemInfo().then(result => {
result = result[1] || {
windowWidth: 375,
windowHeight: 736
};
this.ratio = result.windowWidth / 750;
this.windowHeight = result.windowHeight;
this.init();
this.initCanvas()
})
},
methods: {
toPx(str) {
if (str.indexOf('%') !== -1) {
return Math.floor(Number(str.replace('%', '')) / 100 * this.containerWidth)
}
if (str.indexOf('rpx') !== -1) {
return Math.floor(Number(str.replace('rpx', '')) * this.ratio)
}
return Math.floor(Number(str.replace('px', '')))
},
initCanvas() {
// #ifdef MP-ALIPAY
const context = uni.createSelectorQuery();
// #endif
// #ifndef MP-ALIPAY
const context = uni.createSelectorQuery().in(this);
// #endif
context.select('.nice-cropper').boundingClientRect();
context.exec(res => {
this.containerWidth = res[0].width;
this.containerHeight = res[0].height;
this.initCut()
})
},
resetCutReact() {
this.ctrlWidth = Math.min(this.toPx(this.cutWidth), this.containerWidth);
if (this.cutHeight) {
this.ctrlHeight = Math.min(this.toPx(this.cutHeight), this.containerHeight)
} else {
this.ctrlHeight = Math.min(this.ctrlWidth, this.containerHeight)
}
const cornerStartX = this.center ? Math.floor((this.containerWidth - this.ctrlWidth) / 2) : 0;
const cornerStartY = this.center ? Math.floor((this.containerHeight - this.ctrlHeight) / 2) : 0;
this.cutRatio = this.ctrlHeight / this.ctrlWidth;
this.corner = {
left: cornerStartX,
right: this.containerWidth - this.ctrlWidth - cornerStartX,
top: cornerStartY,
bottom: this.containerHeight - this.ctrlHeight - cornerStartY
}
},
initCut() {
this.resetCutReact();
this.initImage()
},
async initImage() {
if (!this.src) return;
uni.getImageInfo({
src: this.src,
success: (res) => {
let err = false;
this.$emit('load', res)
this.image.originWidth = err ? this.ctrlWidth : res.width;
this.image.originHeight = err ? this.ctrlHeight : res.height;
this.image.width = this.fit ? this.ctrlWidth : this.image.originWidth;
this.image.height = err ? this.ctrlHeight : res.height / res.width * this.image
.width;
this.img = res.path;
const offset = [0, 0];
if (this.imageCenter) {
offset[0] = (this.ctrlWidth - this.image.width) / 2;
offset[1] = (this.ctrlHeight - this.image.height) / 2
}
offset[0] += this.offset[0] || 0;
offset[1] += this.offset[1] || 0;
this.setTranslate(offset);
this.setZoom(this.zoom);
this.transform.angle = this.freeBoundDetect || !this.disableRotate ? this.angle : 0;
this.setBoundary();
this.preview();
// h5打开后不操作可能会出现没有画到画布的情况这里延迟一下就解决了统一加上延迟吧
setTimeout(() => {
this.draw()
}, 100)
},
fail: err => {
if (err) {
this.$emit("error", err)
}
}
});
},
init() {
this.pretouch = {};
this.handles = {};
this.preVector = {
x: 0,
y: 0
};
this.distance = 30;
this.touch = {};
this.movetouch = {};
this.cutMode = false;
this.params = {
zoom: 1,
deltaX: 0,
deltaY: 0,
diffX: 0,
diffY: 0,
angle: 0
}
},
start(e) {
if (!this.src) e.preventDefault();
const point = e.touches ? e.touches[0] : e;
const touch = this.touch;
const now = Date.now();
touch.startX = point.pageX;
touch.startY = point.pageY;
touch.startTime = now;
this.doubleTap = false;
this.view = false;
clearTimeout(this.previewTimer);
if (e.touches.length > 1) {
var point2 = e.touches[1];
this.preVector = {
x: point2.pageX - this.touch.startX,
y: point2.pageY - this.touch.startY
};
this.startDistance = calcLen(this.preVector)
} else {
let pretouch = this.pretouch;
this.doubleTap = this.pretouch.time && now - this.pretouch.time < 300 && ABS(touch.startX - pretouch
.startX) < 30 && ABS(touch.startY - pretouch.startY) < 30 && ABS(touch.startTime - pretouch
.time) < 300;
pretouch = {
startX: this.touch.startX,
startY: this.touch.startY,
time: this.touch.startTime
}
}
},
move(e) {
if (!this.src) return;
const point = e.touches ? e.touches[0] : e;
if (e.touches.length > 1) {
const point2 = e.touches[1];
const v = {
x: point2.pageX - point.pageX,
y: point2.pageY - point.pageY
};
if (this.preVector.x !== null) {
if (this.startDistance) {
const len = calcLen(v);
this.params.zoom = calcLen(v) / this.startDistance;
this.startDistance = len;
this.cutMode && !this.disableCtrl ? this.setCut() : !this.disableScale && this.pinch()
}
this.params.angle = calcAngle(v, this.preVector);
this.cutMode && !this.disableCtrl ? this.setCut() : !this.disableRotate && this.rotate()
}
this.preVector.x = v.x;
this.preVector.y = v.y
} else {
const diffX = point.pageX - this.touch.startX;
const diffY = point.pageY - this.touch.startY;
this.params.diffY = diffY;
this.params.diffX = diffX;
if (this.movetouch.x) {
this.params.deltaX = point.pageX - this.movetouch.x;
this.params.deltaY = point.pageY - this.movetouch.y
} else {
this.params.deltaX = this.params.deltaY = 0
}
if (ABS(diffX) > 30 || ABS(diffY) > 30) {
this.doubleTap = false
}
this.cutMode && !this.disableCtrl ? this.setCut() : !this.disableTranslate && this.translate();
this.movetouch.x = point.pageX;
this.movetouch.y = point.pageY
}!this.cutMode && this.setBoundary();
if (e.touches.length > 1) {
e.preventDefault()
}
},
end() {
this.doubleTap && this.$emit('doubleTap');
this.cutMode && this.setBoundary();
this.init();
!this.disablePreview && this.preview();
this.draw()
},
translate() {
const transform = this.transform.translate;
const meta = this.params;
transform.x += meta.deltaX;
transform.y += meta.deltaY
},
pinch() {
this.transform.zoom *= this.params.zoom
},
rotate() {
this.transform.angle += this.params.angle
},
setZoom(scale) {
scale = Math.min(Math.max(Number(scale) || 1, this.minZoom), this.maxZoom);
this.transform.zoom = scale
},
setTranslate(offset) {
if (Array.isArray(offset)) {
const x = Number(offset[0]);
const y = Number(offset[1]);
this.transform.translate.x = isNaN(x) ? this.transform.translate.x : this.corner.left + x;
this.transform.translate.y = isNaN(y) ? this.transform.translate.y : this.corner.top + y
}
},
setRotate(angle) {
this.transform.angle = Number(angle) || 0
},
setTransform(x, y, angle, scale) {
this.setTranslate([x, y]);
this.setZoom(scale);
this.setRotate(angle)
},
setCutMode(type) {
if (!this.src) return;
this.cutMode = true;
this.cutDirection = type
},
setCut() {
const corner = this.corner;
const meta = this.params;
this.setMeta(this.cutDirection, meta);
if (this.keepRatio) {
if (this.cutDirection === 'lt' || this.cutDirection === 'rb') {
meta.deltaY = meta.deltaX * this.cutRatio
} else {
meta.deltaX = meta.deltaY / this.cutRatio
}
}
switch (this.cutDirection) {
case 'lt':
corner.top += meta.deltaY;
corner.left += meta.deltaX;
break;
case 'rt':
corner.top += meta.deltaY;
corner.right -= this.keepRatio ? -meta.deltaX : meta.deltaX;
break;
case 'rb':
corner.right -= meta.deltaX;
corner.bottom -= meta.deltaY;
break;
case 'lb':
corner.bottom -= meta.deltaY;
corner.left += this.keepRatio ? -meta.deltaX : meta.deltaX;
break
}
this.ctrlWidth = this.containerWidth - corner.left - corner.right;
this.ctrlHeight = this.containerHeight - corner.top - corner.bottom
},
setMeta(direction, meta) {
const {
ctrlWidth,
ctrlHeight,
minWidth,
minHeight
} = this;
switch (direction) {
case 'lt':
if (meta.deltaX > 0 || meta.deltaY > 0) {
meta.deltaX = Math.min(meta.deltaX, ctrlWidth - minWidth);
meta.deltaY = Math.min(meta.deltaY, ctrlHeight - minHeight)
}
break;
case 'rt':
if (meta.deltaX < 0 || meta.deltaY > 0) {
meta.deltaX = Math.max(meta.deltaX, minWidth - ctrlWidth);
meta.deltaY = Math.min(meta.deltaY, ctrlHeight - minHeight)
}
break;
case 'rb':
if (meta.deltaX < 0 || meta.deltaY < 0) {
meta.deltaX = Math.max(meta.deltaX, minWidth - ctrlWidth);
meta.deltaY = Math.max(meta.deltaY, minHeight - ctrlHeight)
}
break;
case 'lb':
if (meta.deltaX > 0 || meta.deltaY < 0) {
meta.deltaX = Math.min(meta.deltaX, ctrlWidth - minWidth);
meta.deltaY = Math.max(meta.deltaY, minHeight - ctrlHeight)
}
break
}
},
setBoundary() {
let zoom = this.transform.zoom;
zoom = zoom < this.minZoom ? this.minZoom : (zoom > this.maxZoom ? this.maxZoom : zoom);
this.transform.zoom = zoom;
if (!this.boundDetect || !this.disableRotate && !this.freeBoundDetect) return true;
const translate = this.transform.translate;
const corner = this.corner;
const minX = corner.left - this.image.width + this.ctrlWidth - this.image.width * (zoom - 1) / 2;
const maxX = corner.left + this.image.width * (zoom - 1) / 2;
const minY = corner.top - this.image.height + this.ctrlHeight - this.image.height * (zoom - 1) / 2;
const maxY = corner.top + this.image.height * (zoom - 1) / 2;
translate.x = Math.floor(translate.x < minX ? minX : (translate.x > maxX ? maxX : translate.x));
translate.y = Math.floor(translate.y < minY ? minY : (translate.y > maxY ? maxY : translate.y))
},
preview() {
clearTimeout(this.previewTimer);
this.previewTimer = setTimeout(() => {
this.view = true
}, 500)
},
draw() {
// #ifdef MP-ALIPAY
const context = uni.createCanvasContext(this.canvasId);
// #endif
// #ifndef MP-ALIPAY
const context = uni.createCanvasContext(this.canvasId, this);
// #endif
const transform = this.transform;
const corner = this.corner;
const canvasZoom = this.canvasZoom;
const img = this.image;
context.save();
context.setFillStyle(this.canvasBackground);
this.$emit('beforeDraw', context, transform);
const zoom = transform.zoom;
context.fillRect(0, 0, this.ctrlWidth * canvasZoom, this.ctrlHeight * canvasZoom);
context.translate((transform.translate.x - corner.left + img.width / 2) * canvasZoom, (transform.translate
.y - corner.top + img.height / 2) * canvasZoom);
context.rotate(transform.angle * Math.PI / 180);
context.translate(-img.width * zoom * 0.5 * canvasZoom, -img.height * zoom * 0.5 * canvasZoom);
context.drawImage(this.img, 0, 0, img.width * zoom * canvasZoom, img.height * zoom * canvasZoom);
context.restore();
this.$emit('afterDraw', context, {
width: this.ctrlWidth * canvasZoom,
height: this.ctrlHeight * canvasZoom
});
context.draw(false);
},
runCrop() {
return new Promise((resolve, reject) => {
// #ifdef MP-ALIPAY
const context = uni.createCanvasContext(this.canvasId);
// #endif
// #ifndef MP-ALIPAY
const context = uni.createCanvasContext(this.canvasId, this);
// #endif
uni.canvasToTempFilePath({
canvasId: this.canvasId,
quality: this.quality || 1,
fileType: this.fileType,
success: (res) => {
// this.$emit('cropped', res.tempFilePath, {
// originWidth: this.image.originWidth,
// originHeight: this.image.originHeight,
// width: this.ctrlWidth * canvasZoom,
// height: this.ctrlHeight * canvasZoom,
// scale: zoom,
// translate: {
// x: transform.translate.x,
// y: transform.translate.y
// },
// rotate: transform.angle
// })
resolve({
url: res.tempFilePath
})
},
fail: err => {
console.error(err)
}
}, this)
})
},
emptyFunc() {
}
}
};

View File

@@ -0,0 +1,93 @@
<template>
<div style="height: 100%;width: 100%; display: flex; justify-content: center; align-items: center; background-color: black;" @mousemove.stop="emptyFunc">
<view class="nice-cropper" :style="{height: height, width: width, background: background}" @touchstart="start"
@touchmove.stop="move" @touchcancel="end" @touchend="end">
<image class="nice-cropper__image" :src="src"
:style="{transform: transformMeta, width: image.width + 'px', height: image.height + 'px'}" />
<view class="nice-cropper__ctrls"
:class="{'nice-cropper__ctrls--view' : view, 'nice-cropper__ctrls--border': showCtrlBorder, 'nice-cropper__ctrls--circle': view && circleView && maskType !== 'outline'}"
:style="ctrlStyle">
<view class="nice-cropper__corner nice-cropper__corner--lt" @touchstart="setCutMode('lt')" />
<view class="nice-cropper__corner nice-cropper__corner--rt" @touchstart="setCutMode('rt')" />
<view class="nice-cropper__corner nice-cropper__corner--rb" @touchstart="setCutMode('rb')" />
<view class="nice-cropper__corner nice-cropper__corner--lb" @touchstart="setCutMode('lb')" />
</view>
<canvas v-if="canvasId" :id="canvasId" :canvas-id="canvasId"
style="position: absolute;left:-500000px;top: -500000px"
:style="{width: ctrlWidth * canvasZoom+'px', height: ctrlHeight * canvasZoom + 'px'}" />
</view>
</div>
</template>
<script src="./cropper.js"></script>
<style>
.nice-cropper {
position: relative;
width: 100%;
height: 100%;
background: #000;
overflow: hidden;
}
.nice-cropper__image {
position: absolute;
left: 0;
top: 0;
transform-origin: 50% 50%;
}
.nice-cropper__corner {
width: 30rpx;
height: 30rpx;
position: absolute;
}
.nice-cropper__corner::after {
position: absolute;
left: -5px;
right: -5px;
bottom: -5px;
top: -5px;
content: '';
}
.nice-cropper__ctrls {
position: absolute;
box-shadow: inset 0 0 10rpx 0 rgba(0, 0, 0, .3);
}
.nice-cropper__ctrls--circle {
border-radius: 50%;
}
.nice-cropper__ctrls--border {
border: 2rpx solid #fff;
}
.nice-cropper__corner--lt {
left: 0;
top: 0;
border-top: 4rpx solid #FFF;
border-left: 4rpx solid #FFF;
}
.nice-cropper__corner--rt {
right: 0;
top: 0;
border-top: 4rpx solid #FFF;
border-right: 4rpx solid #FFF;
}
.nice-cropper__corner--rb {
right: 0;
bottom: 0;
border-right: 4rpx solid #FFF;
border-bottom: 4rpx solid #FFF;
}
.nice-cropper__corner--lb {
left: 0;
bottom: 0;
border-left: 4rpx solid #FFF;
border-bottom: 4rpx solid #FFF;
}
</style>

View File

@@ -0,0 +1,507 @@
<template>
<div>
<uni-popup ref="popup" type="bottom" @change="onPopupChange">
<div class="popup-root">
<form @submit="onSubmit">
<div class="header">{{ title }}</div>
<div v-if="options.desc" class="desc">{{ options.desc }}</div>
<div class="content">
<!-- #ifdef MP-ALIPAY -->
<!-- <template v-if="formData.avatarUrl==null">
<button class="btn" type="primary" open-type="getAuthorize" scope="userInfo"
@getUserInfo="onGetUserInfo">
授权获取头像和昵称
</button>
</template>
<template v-else>
<div v-if="!options.avatarUrl.disable" class="section-line">
<div class="section-line-title">头像</div>
<div class="avatar-wrapper flex-cnsc">
<image mode="aspectFit" class="avatar" :src="formData.avatarUrl">
</image>
</div>
</div>
<div v-if="!options.nickName.disable" class="section-line">
<div class="section-line-title">昵称</div>
<div class="section-line-inputText">{{formData.nickName}}</div>
</div>
</template> -->
<!-- #endif -->
<!-- #ifdef MP-WEIXIN || MP-ALIPAY -->
<div v-if="!options.avatarUrl.disable" class="section-line">
<div class="section-line-title">头像</div>
<button
class="avatar-wrapper flex-cnsc"
@chooseavatar="tapAvatar"
open-type="chooseAvatar"
>
<image mode="aspectFit" class="avatar" :src="formData.avatarUrl"></image>
</button>
</div>
<div v-if="!options.nickName.disable" class="section-line">
<div class="section-line-title">昵称</div>
<input
class="section-line-inputText"
type="nickname"
name="nickName"
placeholder="请输入昵称"
v-model="formData.nickName"
maxlength="16"
/>
</div>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN || MP-ALIPAY-->
<div v-if="!options.avatarUrl.disable" class="section-line">
<div class="section-line-title">头像</div>
<button class="avatar-wrapper flex-cnsc" @click="tapChooseImage">
<image mode="aspectFit" class="avatar" :src="formData.avatarUrl"></image>
</button>
</div>
<div v-if="!options.nickName.disable" class="section-line">
<div class="section-line-title">昵称</div>
<input
class="section-line-inputText"
type="nickname"
name="nickName"
placeholder="请输入昵称"
v-model="formData.nickName"
maxlength="16"
/>
</div>
<!-- #endif -->
</div>
<div class="footer">
<button type="default" class="btn btn-cancel" @click="tapCancel">取消</button>
<button type="primary" formType="submit" class="btn flex-cncc">确认</button>
</div>
</form>
<div :style="{ height: paddingBottom }"></div>
</div>
</uni-popup>
<ImageCropper ref="imageCropper"></ImageCropper>
</div>
</template>
<script>
import ImageCropper from './ImageCropper/ImageCropper.vue'
import UniPopup from './uni-popup/uni-popup.vue'
import * as upload from './upload.js'
function isObject(target) {
return Object.prototype.toString.call(target) === '[object Object]'
}
function isArray(target) {
return Object.prototype.toString.call(target) === '[object Array]'
}
function deepMerge(target, other) {
if (isObject(target) && isObject(other)) {
for (let [key, val] of Object.entries(other)) {
if (isObject(val) || isArray(val)) {
target[key] = deepMerge(target[key], val)
} else {
target[key] = val
}
}
} else if (isArray(target) && isArray(other)) {
for (let [key, val] of Object.entries(other)) {
if (isObject(val) || isArray(val)) {
target[key] = deepMerge(target[key], val)
} else {
target.push(val)
}
}
}
return target
}
/**
* 判断字符串是否为空
*/
function isEmptyStr(str) {
return str == null || str.trim().length == 0
}
export default {
name: 'UserProfileDialog',
props: {
paddingBottom: {
type: String,
default: '0rpx'
},
// 头像的尺寸微信会获取头像时会先调出裁剪框进行裁剪返回已经是宽度为132的图片为了兼容其他平台或者微信没有裁剪的情况如果获取到的图片超过132才再进行裁剪
imgWidth: {
type: Number,
default: 132
}
},
components: {
ImageCropper,
UniPopup
},
data() {
return {
title: '请输入头像和昵称',
confirmFunc: () => {},
cancelFunc: () => {},
cancel: false,
formData: {
avatarUrl: null,
nickName: null
},
options: {
desc: null, //描述
nickName: {
requried: true, // 是否必填
disable: false // 是否隐藏
},
avatarUrl: {
requried: true, // 是否必填
disable: false // 是否隐藏
}
},
isCanEdit: true
}
},
methods: {
show(options, defaultData) {
if (defaultData?.avatarUrl && defaultData?.nickName) {
this.formData.avatarUrl = defaultData.avatarUrl
this.formData.nickName = defaultData.nickName
} else {
this.formData = {
avatarUrl: null,
nickName: null
}
}
if (options) {
deepMerge(this.options, options)
// console.log("合并结果", this.options, options)
}
let titleEle = []
if (!this.options.avatarUrl.disable) {
titleEle.push('头像')
}
if (!this.options.nickName.disable) {
titleEle.push('昵称')
}
this.title = '请输入' + titleEle.join('和')
this.tips = {
avatar: '请授上传头像',
nickName: '请授输入昵称'
}
this.cancel = false
// #ifdef MP-ALIPAY
// 支付宝小程序,尝试获取用户信息
// this.isCanEdit = false;
// this.title = this.title.replace("输入", "授权获取")
// uni.getOpenUserInfo({
// success: (res) => {
// let userInfo = JSON.parse(res.response).response
// this.formData.avatarUrl = userInfo.avatar;
// this.formData.nickName = userInfo.nickName;
// },
// fail: (err) => {
// console.log(err)
// }
// });
// this.tips = {
// avatar: "请授权获取头像",
// nickName: "请授权获取昵称"
// }
// #endif
return new Promise((resolve, reject) => {
this.$refs['popup'].open()
this.confirmFunc = resolve
this.cancelFunc = reject
})
},
// tapTest() {
// let self = this;
// uni.chooseImage({
// // 可以根据您的业务按需选择使用 ['album'] 或 ['camera'] 或 ['album', 'camera']
// sourceType: ['album'],
// success: function(res) {
// console.log(res);
// self.$refs["imageCropper"].show({
// w: 250,
// h: 250,
// avatarUrl: res.tempFilePaths[0]
// })
// },
// fail: function(err) {
// console.log(err);
// },
// });
// },
tapAvatar(e) {
// console.log("结果!!", e)
new Promise((resolve, reject) => {
uni.getImageInfo({
src: e.detail.avatarUrl,
success: res => {
// console.log("图片尺寸", res)
resolve(res)
},
fail: err => {
// console.error("获取图片信息失败了", err)
reject(err)
}
})
})
.then(res => {
if (res.width > this.imgWidth || res.height > this.imgWidth) {
return this.$refs['imageCropper'].show({
w: this.imgWidth,
h: this.imgWidth,
avatarUrl: e.detail.avatarUrl
})
} else {
return Promise.resolve({
tempFilePath: e.detail.avatarUrl
})
}
})
.then(res => {
console.log(res)
return upload.uploadFile(res).then(res => {
this.formData.avatarUrl = res.url
})
})
},
tapChooseImage() {
uni
.chooseImage({
count: 1
})
.then(res => {
console.log(res)
let tempFilePath = null
// #ifdef H5 || APP
tempFilePath = res[res.length - 1].tempFilePaths[0]
// #endif
// #ifdef MP
tempFilePath = res.tempFilePaths[0]
// #endif
this.tapAvatar({
detail: {
avatarUrl: tempFilePath
}
})
})
},
onGetUserInfo(e) {
this.formData.avatarUrl = e.detail.userInfo.avatar
this.formData.nickName = e.detail.userInfo.nickName
},
onSubmit(e) {
// console.log("!!!", this.formData)
if (e.detail.value.nickName) {
this.formData.nickName = e.detail.value.nickName
}
if (
!this.formData.avatarUrl &&
!this.options.avatarUrl.disable &&
this.options.avatarUrl.requried
) {
uni.showToast({
icon: 'none',
title: this.tips.avatar
})
return
}
// console.log(Util.isEmptyStr(this.formData.nickName), !this.options.nickName.disable, this.options.nickName
// .requried)
if (
isEmptyStr(this.formData.nickName) &&
!this.options.nickName.disable &&
this.options.nickName.requried
) {
uni.showToast({
icon: 'none',
title: this.tips.nickName
})
return
}
let resultData = Object.assign({}, this.formData)
this.confirmFunc(resultData)
this.$refs['popup'].close()
},
close() {
this.$refs['popup'].close()
},
tapCancel() {
this.cancel = true
this.$refs['popup'].close()
},
onPopupChange(e) {
if (!e.show) {
this.cancelFunc(this.cancel ? 'cancel' : 'close')
}
}
}
}
</script>
<style lang="scss" scoped>
.popup-root {
background-color: white;
border-top-left-radius: 30rpx;
border-top-right-radius: 30rpx;
//height: 600rpx;
}
.header {
width: 100%;
box-sizing: border-box;
padding: 32rpx;
text-align: center;
font-weight: bold;
}
.desc {
width: 100%;
box-sizing: border-box;
text-align: center;
color: #666;
font-size: 22rpx;
}
.content {
width: 100%;
padding: 16rpx;
box-sizing: border-box;
margin: 20rpx 0;
}
.footer {
width: 100%;
display: flex;
justify-content: center;
box-sizing: border-box;
margin-top: 240rpx;
.btn {
width: 200rpx;
height: 70rpx;
border-radius: 16rpx;
font-size: 32rpx;
display: flex;
justify-content: center;
align-items: center;
line-height: unset;
//border-radius: 2rpx;
border: none;
margin: unset;
&::after {
border: none;
}
}
.btn-cancel {
// background-color: #f8f8f8;
margin-right: 40rpx;
}
}
.section-line {
position: relative;
display: flex;
width: 100%;
padding: 0;
align-items: center;
background-color: white;
}
.section-line-title {
font-size: 32rpx;
color: #818181;
width: 120rpx;
text-align: right;
margin-right: 60rpx;
flex-shrink: 0;
}
.section-line-inputText {
font-size: 32rpx;
height: 118rpx;
text-align: left;
line-height: 118rpx;
}
.section-line:nth-child(n + 2)::after {
content: ' ';
position: absolute;
top: 0;
right: 0;
width: 714rpx;
height: 2rpx !important;
background-color: #f5f5f5;
}
.avatar-wrapper {
box-sizing: border-box;
height: 110rpx;
width: 100%;
& {
padding: unset;
background-color: transparent;
border-radius: 0;
border: none;
box-sizing: unset;
margin: unset;
line-height: normal;
}
&::after {
border: none;
border-style: none;
border-width: 0;
border-radius: 0;
}
}
.avatar {
width: 100rpx;
height: 100rpx;
margin-left: 10rpx;
box-sizing: border-box;
//border: 2rpx #ccc dashed;
}
.flex-cnsc {
display: flex;
justify-content: flex-start;
align-items: center;
}
.flex-cncc {
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,98 @@
# 获取昵称、头像的弹窗,适用最新微信小程序
> uniapp 获取昵称、头像弹窗适用最新微信小程序官方回收getUserProfile接口后使用button的开放能力chooseAvatar替换获取头像和昵称。
> 当选择的图片宽高其中一个超过132px时将用自带图片裁剪功能进行裁剪
## 平台兼容
| H5 | 微信小程序| 支付宝小程序 | 百度小程序| 头条小程序| QQ 小程序 | App |
| --- | ----------| ------------ | ----------| ----------| --------- | --- |
| √ | √ | √ | 未测 | 未测 | 未测 | 未测 |
## 代码演示
### 基本用法
```html
<view>
<cui-userprofiledialog ref="userProfileDialog" paddingBottom="92rpx"></cui-userprofiledialog>
</view>
```
```js
// 页面内调用:
export default {
methods: {
tapGetUserProfile() {
// 使用promise语法
this.$refs["userProfileDialog"].show({
desc: "用于显示个人资料", // 说明
avatarUrl: {
requried: true, // 是否必填
disable: false, // 是否隐藏
},
nickName: {
requried: true, // 是否必填
disable: false, // 是否隐藏
}
}).then(res => {
console.log("结果!!!", res.avatarUrl, res.nickName)
}, err => {
console.log("取消")
});
},
}
}
```
```js
// 上传逻辑单独放在upload.js中之后更新代码更方便
// 注释掉的代码是把图片上传到腾讯云存储建议可以换成自己的上传逻辑返回的图片url就可以直接用了
export function uploadFile(res) {
// 使用本地链接
return Promise.resolve({
url: res.tempFilePath
})
// // 上传到腾讯云
// wxapi.showLoading({
// title: '上传中'
// });
// let file = {
// subKey: 'avatar/' + res.tempFilePath.substring(res.tempFilePath
// .lastIndexOf('/') + 1),
// path: res.tempFilePath,
// size: res.fileSize
// };
// return CosWrap.postObject(file.subKey, file).then(cosRes => {
// wxapi.hideLoading();
// console.log('上传成功', cosRes); // 上传成功
// return Promise.resolve({
// url: cosRes.Location
// })
// }, err => {
// Util.showError(err, "上传");
// wxapi.hideLoading();
// });
}
```
### 插件标签
- 默认 UserProfileDialog 为 component
- ImageCropper 为图片裁剪组件,当选择的图片宽高其中一个超过132px时才会调用自带图片裁剪功能进行裁剪
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --------------| ------------ | ---------------- | ------------ |
| paddingBottom | 下边距 | <em>String</em> | `0rpx` |
| imgWidth | 头像最大尺寸 | <em>Number</em> | 132 |
### 常见问题
- 依赖 uni-popup为了方便使用打包进组件包里了如果项目已经依赖了uni-popup可以删掉以节约空间
### 示例小程序
![](https://web.wodlong.com/common/qrcode-xbrys.jpg)
![](https://web.wodlong.com/common/qrcode-cui-zfb.jpg)
![](https://web.wodlong.com/common/qrcode-h5.png)

View File

@@ -0,0 +1,26 @@
export default {
data() {
return {
}
},
created(){
this.popup = this.getParent()
},
methods:{
/**
* 获取父元素实例
*/
getParent(name = 'uniPopup') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
}
}

View File

@@ -0,0 +1,397 @@
<template>
<view v-if="showPopup" class="uni-popup" :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']"
@touchmove.stop.prevent="clear">
<view @touchstart="touchstart">
<uni-transition key="1" v-if="maskShow" name="mask" mode-class="fade" :styles="maskClass"
:duration="duration" :show="showTrans" @click="onTap" />
<uni-transition key="2" :mode-class="ani" name="content" :styles="transClass" :duration="duration"
:show="showTrans" @click="onTap">
<view class="uni-popup__wrapper" :style="{ backgroundColor: bg }" :class="[popupstyle]" @click="clear">
<slot />
</view>
</uni-transition>
</view>
</view>
</template>
<script>
/**
* PopUp 弹出层
* @description 弹出层组件,为了解决遮罩弹层的问题
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value left 左侧弹出
* @value right 右侧弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} animation = [ture|false] 是否开启动画
* @property {Boolean} maskClick = [ture|false] 蒙版点击是否关闭弹窗
* @property {String} backgroundColor 主窗口背景色
* @property {Boolean} safeArea 是否适配底部安全区
* @event {Function} change 打开关闭弹窗触发e={show: false}
* @event {Function} maskClick 点击遮罩触发
*/
import UniTransition from "./uni-transition/uni-transition.vue"
export default {
name: 'uniPopup',
components: {
UniTransition
},
emits: ['change', 'maskClick'],
props: {
// 开启动画
animation: {
type: Boolean,
default: true
},
// 弹出层类型可选值top: 顶部弹出层bottom底部弹出层center全屏弹出层
// message: 消息提示 ; dialog : 对话框
type: {
type: String,
default: 'center'
},
// maskClick
maskClick: {
type: Boolean,
default: true
},
backgroundColor: {
type: String,
default: 'none'
},
safeArea: {
type: Boolean,
default: true
}
},
watch: {
/**
* 监听type类型
*/
type: {
handler: function(type) {
if (!this.config[type]) return
this[this.config[type]](true)
},
immediate: true
},
isDesktop: {
handler: function(newVal) {
if (!this.config[newVal]) return
this[this.config[this.type]](true)
},
immediate: true
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick: {
handler: function(val) {
this.mkclick = val
},
immediate: true
}
},
data() {
return {
duration: 300,
ani: [],
showPopup: false,
showTrans: false,
popupWidth: 0,
popupHeight: 0,
config: {
top: 'top',
bottom: 'bottom',
center: 'center',
left: 'left',
right: 'right',
message: 'top',
dialog: 'center',
share: 'bottom'
},
maskClass: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)'
},
transClass: {
position: 'fixed',
left: 0,
right: 0
},
maskShow: true,
mkclick: true,
popupstyle: this.isDesktop ? 'fixforpc-top' : 'top'
}
},
computed: {
isDesktop() {
return this.popupWidth >= 500 && this.popupHeight >= 500
},
bg() {
if (this.backgroundColor === '' || this.backgroundColor === 'none') {
return 'transparent'
}
return this.backgroundColor
}
},
mounted() {
const fixSize = () => {
const {
windowWidth,
windowHeight,
windowTop,
safeAreaInsets
} = uni.getSystemInfoSync()
this.popupWidth = windowWidth
this.popupHeight = windowHeight + windowTop
// 是否适配底部安全区
if (this.safeArea) {
this.safeAreaInsets = safeAreaInsets
} else {
this.safeAreaInsets = 0
}
}
fixSize()
},
created() {
this.mkclick = this.maskClick
if (this.animation) {
this.duration = 300
} else {
this.duration = 0
}
// TODO 处理 message 组件生命周期异常的问题
this.messageChild = null
// TODO 解决头条冒泡的问题
this.clearPropagation = false
},
methods: {
/**
* 公用方法,不显示遮罩层
*/
closeMask() {
this.maskShow = false
},
/**
* 公用方法,遮罩层禁止点击
*/
disableMask() {
this.mkclick = false
},
// TODO nvue 取消冒泡
clear(e) {
// #ifndef APP-NVUE
e.stopPropagation()
// #endif
this.clearPropagation = true
},
open(direction) {
let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share']
if (!(direction && innerType.indexOf(direction) !== -1)) {
direction = this.type
}
if (!this.config[direction]) {
console.error('缺少类型:', direction)
return
}
this[this.config[direction]]()
this.$emit('change', {
show: true,
type: direction
})
},
close(type) {
this.showTrans = false
this.$emit('change', {
show: false,
type: this.type
})
clearTimeout(this.timer)
// // 自定义关闭事件
// this.customOpen && this.customClose()
this.timer = setTimeout(() => {
this.showPopup = false
}, 300)
},
// TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
touchstart() {
this.clearPropagation = false
},
onTap() {
if (this.clearPropagation) {
// fix by mehaotian 兼容 nvue
this.clearPropagation = false
return
}
this.$emit('maskClick')
if (!this.mkclick) return
this.close()
},
/**
* 顶部弹出样式处理
*/
top(type) {
this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
this.ani = ['slide-top']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
backgroundColor: this.bg
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
this.$nextTick(() => {
if (this.messageChild && this.type === 'message') {
this.messageChild.timerClose()
}
})
},
/**
* 底部弹出样式处理
*/
bottom(type) {
this.popupstyle = 'bottom'
this.ani = ['slide-bottom']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
paddingBottom: (this.safeAreaInsets && this.safeAreaInsets.bottom) || 0,
backgroundColor: this.bg
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
/**
* 中间弹出样式处理
*/
center(type) {
this.popupstyle = 'center'
this.ani = ['zoom-out', 'fade']
this.transClass = {
position: 'fixed',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
bottom: 0,
left: 0,
right: 0,
top: 0,
justifyContent: 'center',
alignItems: 'center'
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
left(type) {
this.popupstyle = 'left'
this.ani = ['slide-left']
this.transClass = {
position: 'fixed',
left: 0,
bottom: 0,
top: 0,
backgroundColor: this.bg,
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
right(type) {
this.popupstyle = 'right'
this.ani = ['slide-right']
this.transClass = {
position: 'fixed',
bottom: 0,
right: 0,
top: 0,
backgroundColor: this.bg,
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
}
}
}
</script>
<style lang="scss" scoped>
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
&.top,
&.left,
&.right {
top: 0;
}
.uni-popup__wrapper {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
/* #endif */
&.left,
&.right {
/* #ifdef H5 */
padding-top: var(--window-top);
/* #endif */
/* #ifndef H5 */
padding-top: 0;
/* #endif */
flex: 1;
}
}
}
.fixforpc-z-index {
/* #ifndef APP-NVUE */
z-index: 999;
/* #endif */
}
.fixforpc-top {
top: 0;
}
</style>

View File

@@ -0,0 +1,114 @@
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
// #endif
class MPAnimation {
constructor(options, _this) {
this.options = options
this.animation = uni.createAnimation(options)
this.currentStepAnimates = {}
this.next = 0
this.$ = _this
}
_nvuePushAnimates(type, args) {
let aniObj = this.currentStepAnimates[this.next]
let styles = {}
if (!aniObj) {
styles = {
styles: {},
config: {}
}
} else {
styles = aniObj
}
if (animateTypes1.includes(type)) {
if (!styles.styles.transform) {
styles.styles.transform = ''
}
let unit = ''
if(type === 'rotate'){
unit = 'deg'
}
styles.styles.transform += `${type}(${args+unit}) `
} else {
styles.styles[type] = `${args}`
}
this.currentStepAnimates[this.next] = styles
}
_animateRun(styles = {}, config = {}) {
let ref = this.$.$refs['ani'].ref
if (!ref) return
return new Promise((resolve, reject) => {
nvueAnimation.transition(ref, {
styles,
...config
}, res => {
resolve()
})
})
}
_nvueNextAnimate(animates, step = 0, fn) {
let obj = animates[step]
if (obj) {
let {
styles,
config
} = obj
this._animateRun(styles, config).then(() => {
step += 1
this._nvueNextAnimate(animates, step, fn)
})
} else {
this.currentStepAnimates = {}
typeof fn === 'function' && fn()
this.isEnd = true
}
}
step(config = {}) {
// #ifndef APP-NVUE
this.animation.step(config)
// #endif
// #ifdef APP-NVUE
this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config)
this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin
this.next++
// #endif
return this
}
run(fn) {
// #ifndef APP-NVUE
this.$.animationData = this.animation.export()
this.$.timer = setTimeout(() => {
typeof fn === 'function' && fn()
}, this.$.durationTime)
// #endif
// #ifdef APP-NVUE
this.isEnd = false
let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref
if(!ref) return
this._nvueNextAnimate(this.currentStepAnimates, 0, fn)
this.next = 0
// #endif
}
}
const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d',
'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY',
'translateZ'
]
const animateTypes2 = ['opacity', 'backgroundColor']
const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom']
animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => {
MPAnimation.prototype[type] = function(...args) {
// #ifndef APP-NVUE
this.animation[type](...args)
// #endif
// #ifdef APP-NVUE
this._nvuePushAnimates(type, args)
// #endif
return this
}
})
export function createAnimation(option, _this) {
if(!_this) return
clearTimeout(_this.timer)
return new MPAnimation(option, _this)
}

View File

@@ -0,0 +1,277 @@
<template>
<view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
</template>
<script>
import { createAnimation } from './createAnimation'
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
*/
export default {
name: 'uniTransition',
emits:['click','change'],
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: [Array, String],
default() {
return 'fade'
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default() {
return {}
}
},
customClass:{
type: String,
default: ''
}
},
data() {
return {
isShow: false,
transform: '',
opacity: 1,
animationData: {},
durationTime: 300,
config: {}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
// 避免上来就执行 close,导致动画错乱
if (this.isShow) {
this.close()
}
}
},
immediate: true
}
},
computed: {
// 生成样式数据
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's'
}
let transform = ''
for (let i in styles) {
let line = this.toLine(i)
transform += line + ':' + styles[i] + ';'
}
return transform
},
// 初始化动画条件
transformStyles() {
return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
}
},
created() {
// 动画默认配置
this.config = {
duration: this.duration,
timingFunction: 'ease',
transformOrigin: '50% 50%',
delay: 0
}
this.durationTime = this.duration
},
methods: {
/**
* ref 触发 初始化动画
*/
init(obj = {}) {
if (obj.duration) {
this.durationTime = obj.duration
}
this.animation = createAnimation(Object.assign(this.config, obj),this)
},
/**
* 点击组件触发回调
*/
onClick() {
this.$emit('click', {
detail: this.isShow
})
},
/**
* ref 触发 动画分组
* @param {Object} obj
*/
step(obj, config = {}) {
if (!this.animation) return
for (let i in obj) {
try {
if(typeof obj[i] === 'object'){
this.animation[i](...obj[i])
}else{
this.animation[i](obj[i])
}
} catch (e) {
console.error(`方法 ${i} 不存在`)
}
}
this.animation.step(config)
return this
},
/**
* ref 触发 执行动画
*/
run(fn) {
if (!this.animation) return
this.animation.run(fn)
},
// 开始过度动画
open() {
clearTimeout(this.timer)
this.transform = ''
this.isShow = true
let { opacity, transform } = this.styleInit(false)
if (typeof opacity !== 'undefined') {
this.opacity = opacity
}
this.transform = transform
// 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常
this.$nextTick(() => {
// TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器
this.timer = setTimeout(() => {
this.animation = createAnimation(this.config, this)
this.tranfromInit(false).step()
this.animation.run()
this.$emit('change', {
detail: this.isShow
})
}, 20)
})
},
// 关闭过度动画
close(type) {
if (!this.animation) return
this.tranfromInit(true)
.step()
.run(() => {
this.isShow = false
this.animationData = null
this.animation = null
let { opacity, transform } = this.styleInit(false)
this.opacity = opacity || 1
this.transform = transform
this.$emit('change', {
detail: this.isShow
})
})
},
// 处理动画开始前的默认样式
styleInit(type) {
let styles = {
transform: ''
}
let buildStyle = (type, mode) => {
if (mode === 'fade') {
styles.opacity = this.animationType(type)[mode]
} else {
styles.transform += this.animationType(type)[mode] + ' '
}
}
if (typeof this.modeClass === 'string') {
buildStyle(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildStyle(type, mode)
})
}
return styles
},
// 处理内置组合动画
tranfromInit(type) {
let buildTranfrom = (type, mode) => {
let aniNum = null
if (mode === 'fade') {
aniNum = type ? 0 : 1
} else {
aniNum = type ? '-100%' : '0'
if (mode === 'zoom-in') {
aniNum = type ? 0.8 : 1
}
if (mode === 'zoom-out') {
aniNum = type ? 1.2 : 1
}
if (mode === 'slide-right') {
aniNum = type ? '100%' : '0'
}
if (mode === 'slide-bottom') {
aniNum = type ? '100%' : '0'
}
}
this.animation[this.animationMode()[mode]](aniNum)
}
if (typeof this.modeClass === 'string') {
buildTranfrom(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildTranfrom(type, mode)
})
}
return this.animation
},
animationType(type) {
return {
fade: type ? 1 : 0,
'slide-top': `translateY(${type ? '0' : '-100%'})`,
'slide-right': `translateX(${type ? '0' : '100%'})`,
'slide-bottom': `translateY(${type ? '0' : '100%'})`,
'slide-left': `translateX(${type ? '0' : '-100%'})`,
'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
}
},
// 内置动画类型与实际动画对应字典
animationMode() {
return {
fade: 'opacity',
'slide-top': 'translateY',
'slide-right': 'translateX',
'slide-bottom': 'translateY',
'slide-left': 'translateX',
'zoom-in': 'scale',
'zoom-out': 'scale'
}
},
// 驼峰转中横线
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase()
}
}
}
</script>
<style></style>

View File

@@ -0,0 +1,29 @@
export function uploadFile(res) {
// // 上传到腾讯云
// wxapi.showLoading({
// title: '上传中'
// });
// let file = {
// subKey: 'avatar/' + res.tempFilePath.substring(res.tempFilePath
// .lastIndexOf('/') + 1),
// path: res.tempFilePath,
// size: res.fileSize
// };
// return CosWrap.postObject(file.subKey, file).then(cosRes => {
// wxapi.hideLoading();
// console.log('上传成功', cosRes); // 上传成功
// return Promise.resolve({
// url: cosRes.Location
// })
// }, err => {
// Util.showError(err, "上传");
// wxapi.hideLoading();
// });
// 使用本地链接
return Promise.resolve({
url: res.tempFilePath
})
}

View File

@@ -0,0 +1,111 @@
<template>
<view class="custom-header">
<view class="header-left" v-if="isBack">
<slot name="left">
<view @tap="BackPage" v-if="!isTopPage" class="center u-nav-slot">
<up-icon name="arrow-left" size="19"></up-icon>
<up-line direction="column" :hairline="false" length="16" margin="0 8px"></up-line>
<up-icon name="home" size="20"></up-icon>
</view>
</slot>
</view>
<view class="header-title">
<slot name="title">
<span class="title-text">{{ titleText }}</span>
</slot>
</view>
<view class="header-right">
<slot name="right"></slot>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 判断当前页面是否为最顶层页面
const isTopPage = ref(false)
onMounted(() => {
checkIsTopPage()
})
const checkIsTopPage = () => {
const pages = getCurrentPages()
console.log('pages---', pages)
isTopPage.value = pages.length <= 1
}
const props = defineProps({
isBack: {
// 是否显示左侧的返回键
type: Boolean,
default: true
},
tapLeft: {
// 左上角的自定回调函数
type: Function
},
backSuccess: {
// 返回上一页成功的回调函数
type: Function
},
titleText: {
type: String,
default: ''
}
})
const BackPage = () => {
if (props.tapLeft) {
props.tapLeft()
return
}
uni.navigateBack({
delta: 1,
success: function () {
setTimeout(() => {
if (props.backSuccess) uni.$emit('backSuccess')
}, 100)
}
})
}
</script>
<style scoped lang="scss">
.custom-header {
width: 750rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background: #fff;
}
.header-left {
position: absolute;
left: 24rpx;
//padding: 5rpx;
}
.header-right {
position: absolute;
right: 24rpx;
}
.u-nav-slot {
border-radius: 100rpx;
border: 1rpx solid $u-border-color;
padding: 3rpx 7rpx;
opacity: 0.8;
}
.title-text {
font-weight: 600;
font-size: 36rpx;
color: var(--color-text-main);
text-align: center;
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<view :class="{ iphone_mb: !isTabPage }" class="page-view" :style="statusBarHeight()">
<view class="page-container relative" :class="systemStore.commonThemeName" :style="pageStyle()">
<slot></slot>
</view>
<!-- 底部安全区域的颜色,解决IOS下导航栏与底部安全要显示不同颜色的问题-->
<div
v-if="showBottomSafeBox"
class="bottom-safe-box"
:style="{ backgroundColor: safeBottomBgColor }"
></div>
<view class="loading-box" v-if="systemStore.showLoading">
<image class="h-[120rpx]" mode="aspectFit" :src="systemStore.shopInfo.global_logo"></image>
<div class="color-text-secondary text-[26rpx] mt-[18rpx]">加载中</div>
</view>
</view>
</template>
<script setup>
import { useSystemStore } from '@/store/system.js'
const systemStore = useSystemStore()
const props = defineProps({
customStyle: {
type: Object,
default() {
return {}
}
},
customStatusStyle: {
type: Object,
default() {
return {}
}
},
isFullPage: {
// 是否全屏,包括手机的状态栏
type: Boolean,
default: false
},
isShoeHead: {
// 是否显示头部
type: Boolean,
default: true
},
isTabPage: {
// 是否是tab页面,tab页面时,会默认给全面屏加一个底部的安全区域,就不需要手动添加,
type: Boolean,
default: false
},
// IOS底部安全区域的背景色
safeBottomBgColor: {
type: String,
default: '#fff'
},
// 是否需要添加底部安全区域
showBottomSafeBox: {
type: Boolean,
default: false
}
})
const statusBarHeight = () => {
if (!props.isFullPage) {
// 非全屏时,增加状态栏高度
return {
paddingTop: `${systemStore.statusBarHeight}px`,
...props.customStatusStyle
}
}
}
const pageStyle = () => {
// 页面内容的样式
return {
...props.customStyle
}
}
onBeforeMount(() => {
systemStore.setShowLoading(false)
})
onUnmounted(() => {
systemStore.setShowLoading(false)
})
</script>
<style scoped lang="scss">
.iphone_mb {
padding-bottom: env(safe-area-inset-bottom);
}
.page-view {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.loading-box {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
flex-direction: column;
top: 0;
z-index: 9999999;
}
.bottom-safe-box {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
z-index: 2;
height: env(safe-area-inset-bottom);
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<!-- 外层的view flex-1之外,还需要加上overflow-hidden,否则在小程序中scroll-view无法继承父盒子的高度-->
<view class="flex-1 h-full overflow-hidden">
<scroll-view
class="h-full w-full overflow-auto"
:show-scrollbar="false"
:scroll-y="true"
:enhanced="true"
:scroll-with-animation="true"
>
<slot></slot>
</scroll-view>
</view>
</template>
<!-- 自动获取剩余高度的滚动组件,去除了的IOS的滚动条 -->
<script setup>
defineProps({
// 外层view的自定义样式
customStyle: {
type: Object,
default: () => {
return {}
}
}
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,202 @@
$clipper-edge-border-width: 6rpx !default;
.t-cropper {
position: fixed;
width: 100%;
height: 100%;
top: 0;
bottom: 0;
z-index: 1000;
overflow: hidden;
.canvas {
position: absolute;
top: 5000px;
left: 5000px;
}
// 裁剪区域
.t-preview-container {
position: fixed;
width: 100%;
height: 100%;
top: 0;
bottom: 0;
z-index: 1000;
opacity: 0;
overflow: hidden;
.preview-body {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #000;
overflow: hidden;
.mask-model {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #000;
opacity: 0.4;
pointer-events: none;
}
.image-wrap {
position: absolute;
.image {
position: absolute;
}
}
// 裁剪框盒子
.frame-box {
position: absolute;
left: 100px;
top: 100px;
width: 200px;
height: 200px;
// 矩形图片
.rect {
position: absolute;
left: -2px;
top: -2px;
width: 100%;
height: 100%;
border: 2rpx solid white;
overflow: hidden;
box-sizing: content-box;
.image-rect {
position: absolute;
.rect-img {
position: absolute;
}
}
}
//裁剪框线条
.line-one {
position: absolute;
width: 100%;
border-top: 1px dashed #ccc;
left: 0;
top: 33.3%;
box-sizing: content-box;
}
.line-two {
position: absolute;
width: 100%;
border-top: 1px dashed #ccc;
left: 0;
top: 66.7%;
box-sizing: content-box;
}
.line-three {
position: absolute;
height: 100%;
border-right: 1px dashed #ccc;
top: 0;
left: 33.3%;
box-sizing: content-box;
}
.line-four {
position: absolute;
height: 100%;
border-right: 1px dashed #ccc;
top: 0;
left: 66.7%;
box-sizing: content-box;
}
.frame-left-top {
position: absolute;
width: 20px;
height: 20px;
left: -8rpx;
top: -8rpx;
border-left: 4rpx solid #fff;
border-top: 4rpx solid #fff;
box-sizing: content-box;
}
.frame-left-bottom {
position: absolute;
width: 20px;
height: 20px;
left: -8rpx;
bottom: -4rpx;
border-left: 4rpx solid #fff;
border-bottom: 4rpx solid #fff;
box-sizing: content-box;
}
.frame-right-top {
position: absolute;
width: 20px;
height: 20px;
right: -4rpx;
top: -8rpx;
border-right: 4rpx solid #fff;
border-top: 4rpx solid #fff;
box-sizing: content-box;
}
.frame-right-bottom {
position: absolute;
width: 20px;
height: 20px;
right: -4rpx;
bottom: -4rpx;
border-right: 4rpx solid #fff;
border-bottom: 4rpx solid #fff;
box-sizing: content-box;
}
}
}
// 底部工具栏
.toolbar {
position: absolute;
width: calc(100% - 64rpx);
height: 100rpx;
left: 0;
bottom: 10rpx;
text-align: center;
display: flex;
justify-content: space-between;
padding: 0 32rpx;
align-items: center;
// IOS 底部安全距离
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.btn-cancel {
width: 112rpx;
font-size: 28rpx;
color: #d5dfe5;
font-weight: bold;
}
.btn-rotate {
width: 112rpx;
font-size: 28rpx;
color: #d5dfe5;
font-weight: bold;
image {
width: 60rpx;
height: 60rpx;
}
}
.btn-confirm {
font-size: 28rpx;
color: #ffffff;
font-weight: bold;
width: 112rpx;
height: 60rpx;
line-height: 60rpx;
background: #07c160;
border-radius: 6rpx;
text-align: center;
}
}
.transit {
transition: width 0.3s, height 0.3s, left 0.3s, top 0.3s, transform 0.3s;
}
}
.showPage {
opacity: 1 !important;
}
}

File diff suppressed because it is too large Load Diff

37
src/enums/index.js Normal file
View File

@@ -0,0 +1,37 @@
// 订单状态枚举
export const orderStateEnums = [
{
value: -1,
label: '已取消'
},
{
value: 0,
label: '代付款'
},
{
value: 1,
label: '代发货'
},
{
value: 2,
label: '代收货'
},
{
value: 3,
label: '交易完成'
}
]
// 售后类型
export const afterSaleTypeEnums = [
{
value: 0,
label: '我要退款(无需退货)',
desc: '未收到货,或与商家协商之后申请退款'
},
{
value: 1,
label: '我要退货退款',
desc: '已收到货,需要退还已收到的货品'
}
]

View File

@@ -0,0 +1,63 @@
import { ref } from 'vue'
/**
*
* @param minutes 分钟数
* @returns {{seconds: 剩余时间,单位为秒>, timeStr: 时间转换成hh:mm格式的字符串}}
*/
export const useCountDown = minutes => {
let seconds = ref(minutes * 60)
let timeStr = ref('')
const intervalId = setInterval(function () {
const minutesRemaining = Math.floor(seconds.value / 60)
const secondsRemaining = seconds.value % 60
// 将分钟和秒数格式化为字符串,确保单个数字前面有零
const formattedMinutes = String(minutesRemaining).padStart(2, '0')
const formattedSeconds = String(secondsRemaining).padStart(2, '0')
timeStr.value = formattedMinutes + ':' + formattedSeconds
if (seconds.value <= 0) {
clearInterval(intervalId)
} else {
seconds.value--
}
}, 1000) // 每秒更新一次
return {
seconds,
timeStr
}
}
// 验证码倒计时
export const useSmsCodeCountDown = () => {
const smsCodeBtnText = ref('获取验证码')
const countdown = ref(60)
let timer = null
const isCountDown = ref(false) // 是否倒计时中
const startCountDownInterval = () => {
if (timer) return
smsCodeBtnText.value = `${countdown.value}秒后重发`
isCountDown.value = true
timer = setInterval(() => {
countdown.value--
if (countdown.value === 0) {
isCountDown.value = false
smsCodeBtnText.value = '获取验证码'
countdown.value = 60
clearInterval(timer)
timer = null
} else {
smsCodeBtnText.value = `${countdown.value}秒后重发`
}
}, 1000)
}
return {
smsCodeBtnText,
isCountDown,
startCountDownInterval
}
}

24
src/main.js Normal file
View File

@@ -0,0 +1,24 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import store from '@/store'
import * as Pinia from 'pinia'
import uviewPlus from 'uview-plus'
import { createUnistorage } from 'pinia-plugin-unistorage'
// #ifdef MP-WEIXIN
import share from '@/utils/share'
// #endif
// 引入UnoCSS
import 'virtual:uno.css'
export function createApp() {
const app = createSSRApp(App)
store.use(createUnistorage())
app.use(store)
app.use(uviewPlus)
// #ifdef MP-WEIXIN
app.mixin(share)
// #endif
return {
app,
Pinia
}
}

69
src/manifest.json Normal file
View File

@@ -0,0 +1,69 @@
{
"name" : "zy",
"appid" : "",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {
"dSYMs" : false
},
"sdkConfigs" : {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "",
"mergeVirtualHostAttributes" : true,
"setting" : {
"urlCheck" : false,
"ignoreUploadUnusedFiles" : false,
"ignoreDevUnusedFiles" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}

71
src/pages.json Normal file
View File

@@ -0,0 +1,71 @@
{
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue",
"^w-(.*)": "@uni-ui/code-ui/components/w-$1/index.vue"
}
},
"pages": [
//pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/home/home",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
},
{
"path": "pages/components/components",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
},
{
"path": "pages/functions/functions",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
},
{
"path": "pages/uploadDemo/uploadDemo",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
}
],
"globalStyle": {
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "订单中心",
"navigationStyle": "custom",
"navigationBarTextStyle": "black"
},
"tabBar": {
"color": "#333333",
"selectedColor": "#2E69FF",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/home/home",
"text": "首页"
},
{
"pagePath": "pages/components/components",
"text": "组件"
},
{
"pagePath": "pages/functions/functions",
"text": "功能"
}
]
}
}

View File

@@ -0,0 +1,46 @@
<template>
<custom-page>
<custom-head title-text="组件"></custom-head>
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
<scroll-page class="content-container">
<view class="list-item" v-for="item in list" :key="item.name" @tap="item.callback">
<view>{{ item.name }}</view>
<up-icon name="arrow-right"></up-icon>
</view>
</scroll-page>
<cu-tabbar></cu-tabbar>
</custom-page>
<city-picker :visible="cityPickerVisible" @cancel="cityPickerVisible = false"></city-picker>
</template>
<script setup>
const cityPickerVisible = ref(false)
const list = ref([
{
name: '省市区选择',
callback() {
cityPickerVisible.value = true
}
},
{
name: '图片视频上传',
callback() {
uni.navigateTo({
url: '/pages/uploadDemo/uploadDemo'
})
}
}
])
</script>
<style lang="scss">
.list-item {
height: 80rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30rpx;
border-bottom: 1rpx solid #ddd;
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<custom-page>
<custom-head title-text="功能"></custom-head>
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
<scroll-page class="content-container">
<div>
<up-radio-group v-model="themeValue" placement="column" @change="groupChange">
<up-radio
:customStyle="{ marginBottom: '8px' }"
v-for="(item, index) in themeList"
:key="index"
:label="item.label"
:name="item.name"
></up-radio>
</up-radio-group>
</div>
<div class="demo-btn">皮肤测试</div>
</scroll-page>
<cu-tabbar></cu-tabbar>
</custom-page>
</template>
<script setup>
import { useSystemStore } from '@/store/system.js'
const systemStore = useSystemStore()
const themeList = ref([
{
name: 'default-theme',
label: '默认主题'
},
{
name: 'red-theme',
label: '红色主题'
}
])
const themeValue = ref('default-theme')
const groupChange = name => {
systemStore.setThemeName(name)
}
</script>
<style lang="scss">
.demo-btn {
background: var(--color-primary);
}
</style>

15
src/pages/home/home.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<custom-page>
<custom-head title-text="演示界面"></custom-head>
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
<scroll-page class="content-container">
<div class="h-[3000rpx]">自适应的内容滚动区域,兼容IOS下不显示滚动条</div>
</scroll-page>
<view class="bg-red-500 h-[200rpx]">占位盒子</view>
<cu-tabbar></cu-tabbar>
</custom-page>
</template>
<script setup></script>
<style lang="scss"></style>

View File

@@ -0,0 +1,14 @@
<template>
<custom-page>
<custom-head title-text="上传组件"></custom-head>
<!-- 使用scroll-page 组件的话,需要加上content-container才能让里面的元素继承外部的高度-->
<scroll-page class="content-container">
<cu-upload-img-video v-model="fileList"></cu-upload-img-video>
</scroll-page>
</custom-page>
</template>
<script setup>
const fileList = ref([])
</script>
<style lang="scss"></style>

6
src/shime-uni.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export {};
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}

View File

@@ -0,0 +1,319 @@
@font-face {
font-family: "iconfont"; /* Project id 4427123 */
src: url('iconfont.woff2?t=1707183252178') format('woff2'),
url('iconfont.woff?t=1707183252178') format('woff'),
url('iconfont.ttf?t=1707183252178') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-linear-shangpudangkou:before {
content: "\e786";
}
.icon-a-Fillpinganyinhang:before {
content: "\e7f0";
}
.icon-a-Fillqianbaozhifu:before {
content: "\e7f1";
}
.icon-a-Fillshuoming:before {
content: "\e7e4";
}
.icon-a-Linearbangzhu:before {
content: "\e7e5";
}
.icon-a-Linearchakanwenjian:before {
content: "\e7e7";
}
.icon-a-Fillbangzhu:before {
content: "\e7e9";
}
.icon-a-Fillcuowu:before {
content: "\e7ea";
}
.icon-a-Linearshijian:before {
content: "\e7eb";
}
.icon-a-Lineartishi:before {
content: "\e7ec";
}
.icon-a-Linearcuowu:before {
content: "\e7ed";
}
.icon-a-linearyonghuzu:before {
content: "\e7ee";
}
.icon-a-linearyonghu:before {
content: "\e7ef";
}
.icon-a-Linearqiehuan:before {
content: "\e7d3";
}
.icon-a-linearyonghu_shanchu:before {
content: "\e7d4";
}
.icon-a-Fillweixinzhifu:before {
content: "\e7d5";
}
.icon-a-Linearxiangji:before {
content: "\e7d6";
}
.icon-a-Linearjianpan_shanchu:before {
content: "\e7d7";
}
.icon-a-Linearjianpan_shouqi:before {
content: "\e7d8";
}
.icon-a-Lineargou:before {
content: "\e7d9";
}
.icon-a-Linearjianhao:before {
content: "\e7da";
}
.icon-a-Fillzhifubao:before {
content: "\e7db";
}
.icon-a-Linearlajitong:before {
content: "\e7dc";
}
.icon-a-Linearxiahuajiantou:before {
content: "\e7dd";
}
.icon-a-Linearrizhi:before {
content: "\e7de";
}
.icon-a-Linearchenggong:before {
content: "\e7df";
}
.icon-a-Linearshouji:before {
content: "\e7e0";
}
.icon-a-Fillyonghu:before {
content: "\e7e1";
}
.icon-a-Linearshuoming:before {
content: "\e7e2";
}
.icon-a-Fillqingchu:before {
content: "\e7e3";
}
.icon-a-Linearzhiding:before {
content: "\e7c2";
}
.icon-a-Linearxiazai:before {
content: "\e7c3";
}
.icon-a-Linearjinru:before {
content: "\e7c4";
}
.icon-a-Linearfanhui1:before {
content: "\e7c5";
}
.icon-a-Fillzhankai_xiala:before {
content: "\e7c6";
}
.icon-a-Linearshezhi:before {
content: "\e7c7";
}
.icon-a-Linearmimayincang:before {
content: "\e7c8";
}
.icon-a-Linearsousuo:before {
content: "\e7c9";
}
.icon-a-Linearnaozhong:before {
content: "\e7ca";
}
.icon-a-Linearerweima:before {
content: "\e7cb";
}
.icon-a-Lineargengduo:before {
content: "\e7cc";
}
.icon-a-linearxiaoxi:before {
content: "\e7cd";
}
.icon-a-Lineardianhua:before {
content: "\e7ce";
}
.icon-a-Linearbianji:before {
content: "\e7cf";
}
.icon-a-Lineartupian:before {
content: "\e7d0";
}
.icon-a-Lineargonggepaixu:before {
content: "\e7d1";
}
.icon-a-Linearjineyincang:before {
content: "\e7d2";
}
.icon-a-linearyonghu_tianjia:before {
content: "\e7b1";
}
.icon-a-Linearxiangxiajiantou:before {
content: "\e7b2";
}
.icon-a-Linearlianxiren:before {
content: "\e7b3";
}
.icon-a-Fillyinhangka:before {
content: "\e7a4";
}
.icon-a-Linearguanbi:before {
content: "\e7b4";
}
.icon-a-Linearshouye:before {
content: "\e7b5";
}
.icon-a-Linearshuaxin:before {
content: "\e7b6";
}
.icon-a-Fillxialaanniu_shouqi:before {
content: "\e7b7";
}
.icon-a-Linearliebiaopaixu:before {
content: "\e7b8";
}
.icon-a-Linearjiahao:before {
content: "\e7b9";
}
.icon-a-Linearjia:before {
content: "\e7ba";
}
.icon-a-Filltishi:before {
content: "\e7bb";
}
.icon-a-Linearkefu:before {
content: "\e7bc";
}
.icon-a-Filldianhua:before {
content: "\e7bd";
}
.icon-a-Fillshijian:before {
content: "\e7be";
}
.icon-a-Fillkefu:before {
content: "\e7c0";
}
.icon-a-Linearshangchuan:before {
content: "\e7c1";
}
.icon-a-Linearhengshuqiehuan:before {
content: "\e7a6";
}
.icon-a-Lineardayin:before {
content: "\e7a7";
}
.icon-a-Linearxiaoxitongzhi:before {
content: "\e7a8";
}
.icon-a-Linearfuzhi:before {
content: "\e7a5";
}
.icon-a-Linearfanhui2:before {
content: "\e7a9";
}
.icon-a-Linearshipinbofang:before {
content: "\e7aa";
}
.icon-a-Linearxiangshangjiantou:before {
content: "\e7ab";
}
.icon-a-Linearmima_jinekejian:before {
content: "\e7ac";
}
.icon-a-linearyonghu_shezhi:before {
content: "\e7ad";
}
.icon-a-Fillchenggong:before {
content: "\e7ae";
}
.icon-a-Linearjian:before {
content: "\e7af";
}
.icon-a-Linearfenxiang:before {
content: "\e7b0";
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

5
src/store/index.js Normal file
View File

@@ -0,0 +1,5 @@
import { createPinia } from 'pinia'
const store = createPinia()
export default store

33
src/store/system.js Normal file
View File

@@ -0,0 +1,33 @@
import { defineStore } from 'pinia'
export const useSystemStore = defineStore({
id: 'system',
state: () => {
return {
statusBarHeight: 0, // 状态栏的高度
phoneInfo: {},
showLoading: false, // 全局的loading
commonThemeName: 'default-theme' // 主题颜色
}
},
actions: {
setStatusBarHeight(height) {
this.statusBarHeight = height
},
setThemeName(name) {
this.commonThemeName = name
},
setShowLoading(value) {
this.showLoading = value
},
init() {
uni.getSystemInfo({
success: e => {
this.phoneInfo = e
this.statusBarHeight = e.statusBarHeight
}
})
}
}
})

39
src/store/tabbar.js Normal file
View File

@@ -0,0 +1,39 @@
import { defineStore } from 'pinia'
export const useTabBarStore = defineStore({
id: 'tabBar',
state: () => {
return {
// 底部tab菜单
tabBarList: [
{
pagePath: '/pages/home/home',
name: 'home',
text: '首页',
icon: 'home',
selectIcon: ''
},
{
name: 'components',
text: '组件',
icon: 'list',
selectIcon: '',
pagePath: '/pages/components/components'
},
{
name: 'functions',
text: '功能',
icon: 'grid',
selectIcon: '',
pagePath: '/pages/functions/functions'
}
],
currentValue: 'home'
}
},
actions: {
setValue(name) {
this.currentValue = name
}
}
})

21
src/store/user.js Normal file
View File

@@ -0,0 +1,21 @@
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
id: 'user',
state: () => {
return {
token: '',
userInfo: {}, // 用户信息
appInfo: {}, // 应用信息
// 按钮权限编码
permissionCodes: [],
// 权限菜单
menuList: [],
// 权限路由
permissionRoutes: []
}
},
unistorage: true,
getters: {},
actions: {}
})

95
src/styles/global.scss Normal file
View File

@@ -0,0 +1,95 @@
// APP下的处理
//.iphone_pb {
// padding-bottom: constant(safe-area-inset-bottom);
// /*兼容 IOS<11.2*/
// padding-bottom: env(safe-area-inset-bottom);
// /*兼容 IOS>11.2*/
//}
//.iphone_mb {
// margin-bottom: constant(safe-area-inset-bottom);
// /*兼容 IOS<11.2*/
// margin-bottom: env(safe-area-inset-bottom);
// /*兼容 IOS>11.2*/
//}
page{
width: 100%;
height:100%;
}
/* 文字基本颜色 */
//--color-text-main: #303133; // 标题,正文用色
//--color-text-regular: #606266; // 辅助文案,描述等文字角色
//--color-text-secondary: #909399; // 描述,脚注等文字用色
//--color-text-disable: #c0c4cc; // 禁用与站位符
//--color-text-link:#5878b4; // 链接跳转的颜色
//
///* 背景颜色 */
//--color-bg-base: #F3F4F5; // 页面默认背景色
//--color-bg-lightcolor: #f7f8f9; // 页面或者内容区块的浅色背景色
//--color-bg-white: #fff; // 内容区块的填充色
//--color-bg-mask:rgba(0, 0, 0, 0.7); // 通用蒙层色
//
//
//// 分割线与边框
//--color-division-line:#e6e6e6; // 通用的分割线色
//--color-border:#dcdcdc // 按钮边框或内容区块边框用色
// 主题色公用类名
.color-text-main{
color:var(--color-text-main)
}
.color-text-regular{
color:var(--color-text-regular)
}
.color-text-secondary{
color:var(--color-text-secondary)
}
.color-text-disable{
color:var(--color-text-disable)
}
.color-text-link{
color:var(--color-text-link)
}
.color-text-warning{
color:var(--color-function-warning)
}
.color-text-success{
color:var(--color-function-success)
}
.color-text-error{
color:var(--color-function-error)
}
.color-bg-base{
background:var(--color-bg-base)
}
.color-bg-lightcolor{
background:var(--color-bg-lightcolor)
}
.color-bg-mask{
background:var(--color-bg-mask)
}
.page-container{
flex:1;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--color-bg-base);
}
.content-container{
flex:1;
flex-direction: column;
overflow: hidden;
}
// 解决uni的toast图标显示位置不准确的问题
:deep(.uni-toast){
display: flex;
flex-direction: column;
align-items: center;
}

View File

@@ -0,0 +1,29 @@
&{
/* 行为相关颜色 */
--color-primary: #00B85B;
--color-function-success: #00b85b;
--color-function-warning: #ff6f27;
--color-function-error: #f13942;
--color-function-prompt:#00673d;
// 以下部分UI未提供,为预留的主题变量,防止变换主题之后导致一些色值不兼容
--buttom-button-text-color: #fff; // 底部的按钮字体颜色
/* 文字基本颜色 */
--color-text-main: #303133; // 标题,正文用色
--color-text-regular: #606266; // 辅助文案,描述等文字角色
--color-text-secondary: #909399; // 描述,脚注等文字用色
--color-text-disable: #c0c4cc; // 禁用与占位符
--color-text-link:#5878b4; // 链接跳转的颜色
/* 背景颜色 */
--color-bg-base: #F3F4F5; // 页面默认背景色
--color-bg-lightcolor: #f7f8f9; // 页面或者内容区块的浅色背景色
--color-bg-white: #fff; // 内容区块的填充色
--color-bg-mask:rgba(0, 0, 0, 0.7); // 通用蒙层色
// 分割线与边框
--color-division-line:#e6e6e6; // 通用的分割线色
--color-border:#dcdcdc; // 按钮边框或内容区块边框用色
}

View File

@@ -0,0 +1,30 @@
&{
$u-primary:red;
/* 行为相关颜色 */
--color-primary: red;
--color-function-success: #00b85b;
--color-function-warning: #ff6f27;
--color-function-error: #f13942;
--color-function-prompt:#00673d;
// 以下部分UI未提供,为预留的主题变量,防止变换主题之后导致一些色值不兼容
--buttom-button-text-color: #fff; // 底部的按钮字体颜色
/* 文字基本颜色 */
--color-text-main: #303133; // 标题,正文用色
--color-text-regular: #606266; // 辅助文案,描述等文字角色
--color-text-secondary: #909399; // 描述,脚注等文字用色
--color-text-disable: #c0c4cc; // 禁用与占位符
--color-text-link:#5878b4; // 链接跳转的颜色
/* 背景颜色 */
--color-bg-base: #F3F4F5; // 页面默认背景色
--color-bg-lightcolor: #f7f8f9; // 页面或者内容区块的浅色背景色
--color-bg-white: #fff; // 内容区块的填充色
--color-bg-mask:rgba(0, 0, 0, 0.7); // 通用蒙层色
// 分割线与边框
--color-division-line:#e6e6e6; // 通用的分割线色
--color-border:#dcdcdc; // 按钮边框或内容区块边框用色
}

79
src/uni.scss Normal file
View File

@@ -0,0 +1,79 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 每个页面公共css */
@import 'uview-plus/theme.scss';
/* 行为相关颜色 */
$uni-color-primary: #00B85B;
$uni-color-success: #4cd964;
$uni-color-warning: #FF6F27;
$uni-color-error: #F13942;
/* 文字基本颜色 */
$uni-text-color: #303133; // 基本色
$uni-text-color-inverse: #fff; // 反色
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #F3F4F5;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16;
/* 图片尺寸 */
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2c405a; // 文章标题颜色
$uni-font-size-title: 20px;
$uni-color-subtitle: #555; // 二级标题颜色
$uni-font-size-subtitle: 18px;
$uni-color-paragraph: #3f536e; // 文章段落颜色
$uni-font-size-paragraph: 15px;

13
src/utils/auth.js Normal file
View File

@@ -0,0 +1,13 @@
import { useUserStore } from '@/store/user.js'
export const handleAuth = () => {
// 用户登录鉴权
const userInfo = useUserStore()
if (!userInfo.token) {
uni.navigateTo({
url: '/pages/wxLogin/wxLogin'
})
return false
}
return true
}

216
src/utils/index.js Normal file
View File

@@ -0,0 +1,216 @@
/**
* 树形数组扁平
* @param {*} tree
* @returns
*/
export function treeToArray(tree) {
return tree.reduce((res, item) => {
const { children, ...i } = item
return res.concat(i, children && children.length ? treeToArray(children) : [])
}, [])
}
/**
* 反向递归获取树状数据列表,递归获取当前数据所有的上级,并且按照层级返回一个扁平化的数组
* @param {Array} treeArray 树状数据
* @param {Object} targetValue 目标值
* @param {String} key 判断的key值
* @param {String} childrenKey 子集的key值
* @returns {Array} 递归出来的扁平化数组,里面的元素是对象
* */
export function getPathArrByTree(treeArray, targetValue, key, childrenKey = 'children') {
function backwardRecursion(arr, target, currentPath = []) {
for (const node of arr) {
const newPath = [...currentPath, node]
// 如果找到目标值,返回当前路径
if (node[key] === target[key]) {
return newPath
}
// 如果当前节点有子节点,递归在子节点中查找
if (node[childrenKey] && node[childrenKey].length > 0) {
const result = backwardRecursion(node[childrenKey], target, newPath)
if (result) {
return result
}
}
}
// 如果在当前分支未找到目标值返回null
return null
}
return backwardRecursion(treeArray, targetValue)
}
/**
* 正向递归获取树状数据列表,递归获取当前数据所有的下级,并且按照层级返回一个扁平化的数组
* @param {Array} treeArray 树状数据
* @param {String} childrenKey 子集的key值
* @returns {Array} 递归出来的扁平化数组,里面的元素是对象
* */
export function recursion(treeArray, childrenKey = 'children') {
function downRecursion(node) {
let objects = []
if (typeof node === 'object') {
objects.push(node)
if (Array.isArray(node[childrenKey])) {
node[childrenKey].forEach(child => {
objects = objects.concat(downRecursion(child))
})
}
}
return objects
}
return treeArray.reduce((acc, curr) => acc.concat(downRecursion(curr)), [])
}
/**
* 是否图片
* @param {*} str
* @returns
*/
export const isImage = str => {
if (str && typeof str === 'string') {
// 定义图片文件扩展名的正则表达式
const imageExtensions = /\.(jpeg|jpg|gif|png|bmp|webp)$/i
return imageExtensions.test(str)
}
return false
}
/**
* @description 文件转base64工具函数
* @param {Object} file 文件对象
* @returns {Promise}
* */
export function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result.split(',')[1])
reader.onerror = error => reject(error)
reader.readAsDataURL(file)
})
}
/**
* @description 根据文件对象获取后缀名
* @param {Object} file 文件对象
* @returns {String}
* */
export function getFileExtension(file) {
const fileName = file.name
const lastDotIndex = fileName.lastIndexOf('.')
const fileExtension = fileName.substring(lastDotIndex + 1)
return fileExtension
}
// 生成随机唯一值
export function generateRandomValue() {
// 生成一个随机的浮点数,然后将其转换为字符串
const randomValue = 'D' + Math.random().toString().substring(2, 5)
// 获取当前时间戳,并转换为字符串
const timestamp = new Date().getTime().toString()
// 将随机值和时间戳拼接在一起
const uniqueRandomValue = randomValue + timestamp
return uniqueRandomValue
}
/**
* 将对象参数转变成url的参数
* @param obj
* @returns {string}
*/
export function objectToUrlParams(obj) {
return Object.keys(obj)
.map(key => `${key}=${obj[key]}`)
.join('&')
}
/**
* 电话号码脱敏
* @param phone
* @returns {String}
*/
export function phoneDesensitization(phone) {
if (!phone) return
// 定义手机号正则表达式
let reg = /^(1[3-9][0-9])\d{4}(\d{4}$)/
// 判断手机号是否能够通过正则校验
let isMobile = reg.test(phone)
console.log(isMobile)
// 将手机号中间4位用*号进行显示
let hiddenMobile = phone.replace(reg, '$1****$2')
return hiddenMobile
}
/**
*
* @param money {number} 后台使用的是分,前端展示一般都是元,所以/100
* @returns {number}
* @constructor
*/
export const moneyConversion = money => {
return money || money === 0 ? (money / 100).toFixed(2) : 0
}
/**
* @param money {number} 带小数点的金额
* @return {Object} {integer:整数,decimals:小数点}
*/
export const moneySplit = money => {
const arr = money.toString().split('.')
return {
integer: arr[0],
decimals: arr[1]
}
}
export const moneyConversionAndSplit = money => {
return moneySplit(moneyConversion(money))
}
/**
*
* @param money {number} 后台使用的是分,前端表单一般都是元,所以 * 100
* @returns {number}
* @constructor
*/
export const submitMoneyConversion = money => {
return +(money * 100).toFixed(2)
}
/**
* 防抖函数
* @param fn
* @param wait
* @returns {(function(): void)|*}
*/
export const debounce = (func, wait) => {
// @TODO实现逻辑
let timeout
return function () {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
//...arguments用过获取参数
func.call(this, ...arguments)
}, wait)
}
}
export const contactService = () => {
uni.makePhoneCall({
phoneNumber: '4008882292' //仅为示例
})
}
export function rpxToPx(rpx) {
const systemInfo = uni.getSystemInfoSync()
const screenWidth = systemInfo.screenWidth // 获取屏幕宽度
return (rpx / 750) * screenWidth
}

13
src/utils/reg.js Normal file
View File

@@ -0,0 +1,13 @@
export default {
userName: /^[\u4e00-\u9fa5a-zA-Z0-9]{2,12}$/,
// 手机号1开头11位
phoneNumber: /^1\d{10}$/,
// 邮箱xxx@yyy.zzz40字符以内
email: /(?=^[a-z0-9.]+@[a-z0-9.-]+\.[a-zA-Z]{2,6}$)(?=^.{0,40}$)/,
// 密码:字母、数字、特殊符号(~!@#$%^&*.,至少包含2种长度6-20
password: /^(?![\d]+$)(?![a-z]+$)(?![A-Z]+$)(?![~!@#$%^&*.]+$)[\da-zA-z~!@?#$%^&*.,。,]{6,20}$/,
// 编码类字段数字或字母5-20个字符
code: /^[a-zA-Z0-9]{5,20}$/,
// 账号可以包含字母区分大小写、数字、下划线_、减号-、邮箱分隔符@、小数点5-25个字符
account: /^[a-zA-Z0-9._\-@]{5,25}$/
}

31
src/utils/share.js Normal file
View File

@@ -0,0 +1,31 @@
import { onShareAppMessage } from '@dcloudio/uni-app'
export default {
onLoad() {
// #ifdef MP-WEIXIN
wx.showShareMenu({
withShareTicket: false,
menus: ['shareAppMessage', 'shareTimeline']
})
// #endif
},
onShareAppMessage() {
const customSharePaths = ['pages/goodsDetail/goodsDetail'] // 不走全局分享配置的路径
// 获取当前页面栈
const pages = getCurrentPages()
// 获取当前路由路径
const currentPath = pages[pages.length - 1]?.route
console.log('currentPath----------', currentPath)
return {}
// if (!customSharePaths.includes(currentPath)) {
// return {
// title: '江楠甄选',
// path: '/pages/home/home',
// imageUrl:
// 'https://cfzy-durian-front.tos-cn-guangzhou.volces.com/zm-mall-mini-app/wx_share_logo.png'
// }
// } else {
// return {}
// }
}
}

44
uno.config.js Normal file
View File

@@ -0,0 +1,44 @@
import { defineConfig, presetIcons, transformerDirectives, transformerVariantGroup } from 'unocss'
import { presetWeapp } from 'unocss-preset-weapp'
import { extractorAttributify, transformerClass } from 'unocss-preset-weapp/transformer'
const { presetWeappAttributify, transformerAttributify } = extractorAttributify()
export default defineConfig({
presets: [
// https://github.com/MellowCo/unocss-preset-weapp
presetWeapp(),
// attributify autocomplete
presetWeappAttributify(),
// https://unocss.dev/presets/icons
presetIcons({
scale: 1.2,
warn: true,
extraProperties: {
display: 'inline-block',
'vertical-align': 'middle'
}
})
],
/**
* 自定义快捷语句
* @see https://github.com/unocss/unocss#shortcuts
*/
shortcuts: {
'border-base': 'border border-gray-500_10',
center: 'flex justify-center items-center'
},
transformers: [
// 启用 @apply 功能
transformerDirectives({
enforce: 'pre'
}),
// https://unocss.dev/transformers/variant-group
// 启用 () 分组功能
transformerVariantGroup(),
// https://github.com/MellowCo/unocss-preset-weapp/tree/main/src/transformer/transformerAttributify
transformerAttributify(),
// https://github.com/MellowCo/unocss-preset-weapp/tree/main/src/transformer/transformerClass
transformerClass()
]
})

38
vite.config.js Normal file
View File

@@ -0,0 +1,38 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import AutoImport from 'unplugin-auto-import/vite'
import { fileURLToPath, URL } from 'node:url'
import UnoCSS from 'unocss/vite'
export default defineConfig({
transpileDependencies: ['z-paging'],
plugins: [
AutoImport({
imports: ['vue'] // vue api 自动导入
}),
uni.default(),
UnoCSS()
],
css: {
preprocessorOptions: {
scss: {
// 取消sass废弃API的报警
silenceDeprecations: ['legacy-js-api', 'color-functions', 'import']
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
minify: 'terser',
terserOptions: {
compress: {
// drop_console: true, // 去除console
// drop_debugger: true // 去除debugger
}
}
}
})

7
webpack.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
resolve: {
alias: {
'@': require('path').resolve(__dirname + '/src')
}
}
}