🎉 init project
This commit is contained in:
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,11 +1,8 @@
|
|||||||
# ---> Vue
|
### Vuejs template
|
||||||
# gitignore template for Vue.js projects
|
|
||||||
#
|
|
||||||
# Recommended template: Node.gitignore
|
# Recommended template: Node.gitignore
|
||||||
|
|
||||||
# TODO: where does this rule come from?
|
node_modules/
|
||||||
docs/_book
|
dist/
|
||||||
|
npm-debug.log
|
||||||
# TODO: where does this rule come from?
|
yarn-error.log
|
||||||
test/
|
.idea
|
||||||
|
|
||||||
|
|||||||
111
README.md
111
README.md
@@ -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>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 封装两个很常用的组件,可以直接下载项目运行查看
|
||||||
|
- 省市区选择
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 上传图片与视频
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|||||||
70
auto-imports.d.ts
vendored
Normal file
70
auto-imports.d.ts
vendored
Normal 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
23
commitlint.config.cjs
Normal 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
20
index.html
Normal 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
28
jsconfig.json
Normal 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
14347
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
121
package.json
Normal file
121
package.json
Normal 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
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
10
shims-uni.d.ts
vendored
Normal 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
27
src/App.vue
Normal 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
111
src/api/http.js
Normal 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
12
src/api/modules/test.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
54
src/common/components/transfer-account-dialog/index.vue
Normal file
54
src/common/components/transfer-account-dialog/index.vue
Normal 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>
|
||||||
90
src/common/components/verification-code-dialog/index.vue
Normal file
90
src/common/components/verification-code-dialog/index.vue
Normal 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>
|
||||||
36
src/components/city-picker/README.md
Normal file
36
src/components/city-picker/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 省市区选择
|
||||||
|
> 省市区街道选择组件,不依赖任何UI框架,直接拷贝也可以使用(**注意JSON文件在小程序中,建议最好放到云端**)
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 使用
|
||||||
|
```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/>} |
|
||||||
|
|
||||||
|
|
||||||
270
src/components/city-picker/city-picker.vue
Normal file
270
src/components/city-picker/city-picker.vue
Normal 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>
|
||||||
1
src/components/city-picker/pcas-code.json
Normal file
1
src/components/city-picker/pcas-code.json
Normal file
File diff suppressed because one or more lines are too long
70
src/components/cu-bottom-dialog/cu-bottom-dialog.vue
Normal file
70
src/components/cu-bottom-dialog/cu-bottom-dialog.vue
Normal 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>
|
||||||
84
src/components/cu-checkbox/cu-checkbox.vue
Normal file
84
src/components/cu-checkbox/cu-checkbox.vue
Normal 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>
|
||||||
60
src/components/cu-middle-dialog/cu-middle-dialog.vue
Normal file
60
src/components/cu-middle-dialog/cu-middle-dialog.vue
Normal 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>
|
||||||
65
src/components/cu-radio/cu-radio.vue
Normal file
65
src/components/cu-radio/cu-radio.vue
Normal 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>
|
||||||
143
src/components/cu-street/cu-street.vue
Normal file
143
src/components/cu-street/cu-street.vue
Normal 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>
|
||||||
33
src/components/cu-tabbar/cu-tabbar.vue
Normal file
33
src/components/cu-tabbar/cu-tabbar.vue
Normal 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>
|
||||||
105
src/components/cu-upload-img-video/README.md
Normal file
105
src/components/cu-upload-img-video/README.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
> 支持H5与小程序选择上传图片与视频,并且支持数量限制,图片,视频大小限制,自带图片视频的预览,也兼容了IOS在小程序中的预览
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 使用
|
||||||
|
```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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
352
src/components/cu-upload-img-video/cu-upload-img-video.vue
Normal file
352
src/components/cu-upload-img-video/cu-upload-img-video.vue
Normal 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>
|
||||||
76
src/components/cu-upload-img-video/upload.js
Normal file
76
src/components/cu-upload-img-video/upload.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
507
src/components/cui-userprofiledialog/cui-userprofiledialog.vue
Normal file
507
src/components/cui-userprofiledialog/cui-userprofiledialog.vue
Normal 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>
|
||||||
98
src/components/cui-userprofiledialog/readme.md
Normal file
98
src/components/cui-userprofiledialog/readme.md
Normal 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,可以删掉以节约空间
|
||||||
|
|
||||||
|
### 示例小程序
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
26
src/components/cui-userprofiledialog/uni-popup/popup.js
Normal file
26
src/components/cui-userprofiledialog/uni-popup/popup.js
Normal 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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
397
src/components/cui-userprofiledialog/uni-popup/uni-popup.vue
Normal file
397
src/components/cui-userprofiledialog/uni-popup/uni-popup.vue
Normal 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>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
29
src/components/cui-userprofiledialog/upload.js
Normal file
29
src/components/cui-userprofiledialog/upload.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
111
src/components/custom-head/custom-head.vue
Normal file
111
src/components/custom-head/custom-head.vue
Normal 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>
|
||||||
125
src/components/custom-page/custom-page.vue
Normal file
125
src/components/custom-page/custom-page.vue
Normal 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>
|
||||||
28
src/components/scroll-page/scroll-page.vue
Normal file
28
src/components/scroll-page/scroll-page.vue
Normal 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>
|
||||||
202
src/components/t-cropper/t-cropper.scss
Normal file
202
src/components/t-cropper/t-cropper.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1030
src/components/t-cropper/t-cropper.vue
Normal file
1030
src/components/t-cropper/t-cropper.vue
Normal file
File diff suppressed because it is too large
Load Diff
37
src/enums/index.js
Normal file
37
src/enums/index.js
Normal 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: '已收到货,需要退还已收到的货品'
|
||||||
|
}
|
||||||
|
]
|
||||||
63
src/hooks/useGlobalUtil.js
Normal file
63
src/hooks/useGlobalUtil.js
Normal 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
24
src/main.js
Normal 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
69
src/manifest.json
Normal 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
71
src/pages.json
Normal 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": "功能"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/pages/components/components.vue
Normal file
46
src/pages/components/components.vue
Normal 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>
|
||||||
50
src/pages/functions/functions.vue
Normal file
50
src/pages/functions/functions.vue
Normal 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
15
src/pages/home/home.vue
Normal 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>
|
||||||
14
src/pages/uploadDemo/uploadDemo.vue
Normal file
14
src/pages/uploadDemo/uploadDemo.vue
Normal 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
6
src/shime-uni.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export {};
|
||||||
|
|
||||||
|
declare module "vue" {
|
||||||
|
type Hooks = App.AppInstance & Page.PageInstance;
|
||||||
|
interface ComponentCustomOptions extends Hooks {}
|
||||||
|
}
|
||||||
319
src/static/font/iconfont.css
Normal file
319
src/static/font/iconfont.css
Normal 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";
|
||||||
|
}
|
||||||
|
|
||||||
1
src/static/font/iconfont.js
Normal file
1
src/static/font/iconfont.js
Normal file
File diff suppressed because one or more lines are too long
BIN
src/static/font/iconfont.ttf
Normal file
BIN
src/static/font/iconfont.ttf
Normal file
Binary file not shown.
BIN
src/static/font/iconfont.woff
Normal file
BIN
src/static/font/iconfont.woff
Normal file
Binary file not shown.
BIN
src/static/font/iconfont.woff2
Normal file
BIN
src/static/font/iconfont.woff2
Normal file
Binary file not shown.
BIN
src/static/images/loading.gif
Normal file
BIN
src/static/images/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
5
src/store/index.js
Normal file
5
src/store/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
const store = createPinia()
|
||||||
|
|
||||||
|
export default store
|
||||||
33
src/store/system.js
Normal file
33
src/store/system.js
Normal 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
39
src/store/tabbar.js
Normal 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
21
src/store/user.js
Normal 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
95
src/styles/global.scss
Normal 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;
|
||||||
|
}
|
||||||
29
src/styles/themes/default-theme.scss
Normal file
29
src/styles/themes/default-theme.scss
Normal 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; // 按钮边框或内容区块边框用色
|
||||||
|
}
|
||||||
|
|
||||||
30
src/styles/themes/red-theme.scss
Normal file
30
src/styles/themes/red-theme.scss
Normal 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
79
src/uni.scss
Normal 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
13
src/utils/auth.js
Normal 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
216
src/utils/index.js
Normal 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
13
src/utils/reg.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default {
|
||||||
|
userName: /^[\u4e00-\u9fa5a-zA-Z0-9]{2,12}$/,
|
||||||
|
// 手机号:1开头,11位
|
||||||
|
phoneNumber: /^1\d{10}$/,
|
||||||
|
// 邮箱:xxx@yyy.zzz,40字符以内
|
||||||
|
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
31
src/utils/share.js
Normal 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
44
uno.config.js
Normal 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
38
vite.config.js
Normal 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
7
webpack.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': require('path').resolve(__dirname + '/src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user