🎉 init project
This commit is contained in:
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,11 +1,8 @@
|
||||
# ---> Vue
|
||||
# gitignore template for Vue.js projects
|
||||
#
|
||||
### Vuejs template
|
||||
# Recommended template: Node.gitignore
|
||||
|
||||
# TODO: where does this rule come from?
|
||||
docs/_book
|
||||
|
||||
# TODO: where does this rule come from?
|
||||
test/
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.idea
|
||||
|
||||
111
README.md
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