203 changed files with 31450 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||
[*.{js,jsx,ts,tsx,vue}] |
|||
indent_style = space |
|||
indent_size = 2 |
|||
end_of_line = lf |
|||
trim_trailing_whitespace = true |
|||
insert_final_newline = true |
|||
max_line_length = 140 |
|||
root = true |
@ -0,0 +1,13 @@ |
|||
node_modules |
|||
dist/ |
|||
test |
|||
build/ |
|||
unpackage/ |
|||
babel.config.js |
|||
package.json |
|||
postcss.config.js |
|||
.eslint.js |
|||
vue.config.js |
|||
src/common/styles/index.css |
|||
src/pages.json |
|||
src/manifest.json |
@ -0,0 +1,21 @@ |
|||
module.exports = { |
|||
"env": { |
|||
"browser": true, |
|||
"es2021": true |
|||
}, |
|||
"extends": [ |
|||
"plugin:vue/essential", |
|||
"airbnb-base" |
|||
], |
|||
"parserOptions": { |
|||
"ecmaVersion": 13, |
|||
"parser": "@typescript-eslint/parser", |
|||
"sourceType": "module" |
|||
}, |
|||
"plugins": [ |
|||
"vue", |
|||
"@typescript-eslint" |
|||
], |
|||
"rules": { |
|||
} |
|||
}; |
@ -0,0 +1,27 @@ |
|||
.DS_Store |
|||
node_modules/ |
|||
unpackage/ |
|||
dist/ |
|||
|
|||
# local env files |
|||
.env.local |
|||
.env.*.local |
|||
|
|||
# Log files |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
|
|||
yarn.lock |
|||
package-lock.json |
|||
|
|||
# Editor directories and files |
|||
.project |
|||
.idea |
|||
.vscode |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw* |
|||
.eslintcache |
@ -0,0 +1,20 @@ |
|||
{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/ |
|||
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数 |
|||
"version": "0.0", |
|||
"configurations": [{ |
|||
"default" : |
|||
{ |
|||
"launchtype" : "local" |
|||
}, |
|||
"h5" : |
|||
{ |
|||
"launchtype" : "local" |
|||
}, |
|||
"mp-weixin" : |
|||
{ |
|||
"launchtype" : "local" |
|||
}, |
|||
"type" : "uniCloud" |
|||
} |
|||
] |
|||
} |
@ -0,0 +1 @@ |
|||
registry=https://registry.npm.taobao.org |
@ -0,0 +1,9 @@ |
|||
node_modules |
|||
dist/ |
|||
test |
|||
build/ |
|||
unpackage/ |
|||
babel.config.js |
|||
package.json |
|||
postcss.config.js |
|||
.eslint.js |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"printWidth": 140, |
|||
"singleQuote": true, |
|||
"semi": true, |
|||
"trailingComma": "all", |
|||
"arrowParens": "avoid", |
|||
"tabWidth": 2, |
|||
"useTabs": false, |
|||
"bracketSpacing": true, |
|||
"jsxBracketSameLine": false, |
|||
"proseWrap": "always", |
|||
"endOfLine": "lf" |
|||
} |
@ -0,0 +1,36 @@ |
|||
<script> |
|||
// import { |
|||
// ref, |
|||
// computed |
|||
// } from 'vue'; |
|||
// import { |
|||
// useStore |
|||
// } from 'vuex'; |
|||
|
|||
// const store = useStore(); |
|||
|
|||
// onLaunch(() => { |
|||
// // checkNetwork(); // 监听网络状态 |
|||
// }); |
|||
|
|||
// 检查网络状态 设置store里的网络状态变量 |
|||
// 网络连接 且 不是2g 不是3g 才算是网络连接; 否则不是 将启用本地存储 |
|||
// function checkNetwork() { |
|||
// uni.getNetworkType({ |
|||
// success: ({ networkType }) => { |
|||
// this.setNetworkConnected(!(networkType === 'none' || networkType === '2g' || networkType === '3g')); |
|||
// }, |
|||
// }); |
|||
// // 监听网络状态的变化 |
|||
// uni.onNetworkStatusChange(({ isConnected, networkType }) => { |
|||
// this.setNetworkConnected(isConnected && !(networkType === '2g' || networkType === '3g')); |
|||
// }); |
|||
// } |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
/*每个页面公共css */ |
|||
@import "@/uni_modules/vk-uview-ui/index.scss"; |
|||
@import '@/common/styles/iconfont.scss'; |
|||
@import '@/common/styles/app.scss'; |
|||
</style> |
@ -0,0 +1,6 @@ |
|||
# 1.0.0 (2021-12-31) |
|||
|
|||
范围|描述|commitId |
|||
--|--|-- |
|||
- | Initial commit | 52b8f49 |
|||
|
@ -0,0 +1 @@ |
|||
module.exports = { extends: ['./node_modules/vue-cli-plugin-commitlint/lib/lint'] }; |
@ -0,0 +1,155 @@ |
|||
.min-0 { |
|||
min-width: 0; |
|||
} |
|||
.border-b-1 { |
|||
border-bottom-width: 1px; |
|||
} |
|||
|
|||
/* 列表 */ |
|||
.uni-list { |
|||
background-color: #ffffff; |
|||
position: relative; |
|||
width: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
border: 1px solid #afbed1; |
|||
height: 80rpx; |
|||
line-height: 80rpx; |
|||
} |
|||
.uni-list-cell { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.uni-list-cell-hover { |
|||
background-color: #eee; |
|||
} |
|||
.uni-list-cell-pd { |
|||
padding: 22rpx 30rpx; |
|||
} |
|||
.uni-list-cell-left { |
|||
white-space: nowrap; |
|||
font-size: 28rpx; |
|||
padding: 0 30rpx; |
|||
} |
|||
.uni-list-cell-db, |
|||
.uni-list-cell-right { |
|||
flex: 1; |
|||
} |
|||
.uni-list-cell::after { |
|||
position: absolute; |
|||
z-index: 3; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 30rpx; |
|||
height: 1px; |
|||
content: ''; |
|||
-webkit-transform: scaleY(0.5); |
|||
transform: scaleY(0.5); |
|||
background-color: #c8c7cc; |
|||
} |
|||
.uni-list .uni-list-cell:last-child::after { |
|||
height: 0rpx; |
|||
} |
|||
.uni-list-cell-last.uni-list-cell::after { |
|||
height: 0rpx; |
|||
} |
|||
.uni-list-cell-divider { |
|||
position: relative; |
|||
display: flex; |
|||
color: #999; |
|||
background-color: #f7f7f7; |
|||
padding: 15rpx 20rpx; |
|||
} |
|||
.uni-list-cell-divider::before { |
|||
position: absolute; |
|||
right: 0; |
|||
top: 0; |
|||
left: 0; |
|||
height: 1px; |
|||
content: ''; |
|||
-webkit-transform: scaleY(0.5); |
|||
transform: scaleY(0.5); |
|||
background-color: #c8c7cc; |
|||
} |
|||
.uni-list-cell-divider::after { |
|||
position: absolute; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0rpx; |
|||
height: 1px; |
|||
content: ''; |
|||
-webkit-transform: scaleY(0.5); |
|||
transform: scaleY(0.5); |
|||
background-color: #c8c7cc; |
|||
} |
|||
.uni-list-cell-navigate { |
|||
font-size: 30rpx; |
|||
padding: 22rpx 30rpx; |
|||
line-height: 48rpx; |
|||
position: relative; |
|||
display: flex; |
|||
box-sizing: border-box; |
|||
width: 100%; |
|||
flex: 1; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.uni-list-cell-navigate { |
|||
padding-right: 36rpx; |
|||
} |
|||
.uni-navigate-badge { |
|||
padding-right: 50rpx; |
|||
} |
|||
.uni-list-cell-navigate.uni-navigate-right:after { |
|||
font-family: uniicons; |
|||
content: '\e583'; |
|||
position: absolute; |
|||
right: 24rpx; |
|||
top: 50%; |
|||
color: #bbb; |
|||
-webkit-transform: translateY(-50%); |
|||
transform: translateY(-50%); |
|||
} |
|||
.uni-list-cell-navigate.uni-navigate-bottom:after { |
|||
font-family: uniicons; |
|||
content: '\e581'; |
|||
position: absolute; |
|||
right: 24rpx; |
|||
top: 50%; |
|||
color: #bbb; |
|||
-webkit-transform: translateY(-50%); |
|||
transform: translateY(-50%); |
|||
} |
|||
.uni-list-cell-navigate.uni-navigate-bottom.uni-active::after { |
|||
font-family: uniicons; |
|||
content: '\e580'; |
|||
position: absolute; |
|||
right: 24rpx; |
|||
top: 50%; |
|||
color: #bbb; |
|||
-webkit-transform: translateY(-50%); |
|||
transform: translateY(-50%); |
|||
} |
|||
.uni-collapse.uni-list-cell { |
|||
flex-direction: column; |
|||
} |
|||
.uni-list-cell-navigate.uni-active { |
|||
background: #eee; |
|||
} |
|||
.uni-list.uni-collapse { |
|||
box-sizing: border-box; |
|||
height: 0; |
|||
overflow: hidden; |
|||
} |
|||
.uni-collapse .uni-list-cell { |
|||
padding-left: 20rpx; |
|||
} |
|||
.uni-collapse .uni-list-cell::after { |
|||
left: 52rpx; |
|||
} |
|||
.uni-list.uni-active { |
|||
height: auto; |
|||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,8 @@ |
|||
<template> |
|||
</template> |
|||
|
|||
<script> |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
@ -0,0 +1,8 @@ |
|||
<template> |
|||
</template> |
|||
|
|||
<script> |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
@ -0,0 +1,8 @@ |
|||
<template> |
|||
</template> |
|||
|
|||
<script> |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
@ -0,0 +1,8 @@ |
|||
<template> |
|||
</template> |
|||
|
|||
<script> |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
@ -0,0 +1,14 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" /> |
|||
<title></title> |
|||
<!--preload-links--> |
|||
<!--app-context--> |
|||
</head> |
|||
<body> |
|||
<div id="app"><!--app-html--></div> |
|||
<script type="module" src="/main.js"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,23 @@ |
|||
import App from './App' |
|||
import uView from './uni_modules/vk-uview-ui'; // 引入 uView UI
|
|||
|
|||
// #ifndef VUE3
|
|||
import Vue from 'vue' |
|||
Vue.config.productionTip = false |
|||
App.mpType = 'app' |
|||
const app = new Vue({ |
|||
...App |
|||
}) |
|||
app.$mount() |
|||
// #endif
|
|||
|
|||
// #ifdef VUE3
|
|||
import { createSSRApp } from 'vue' |
|||
export function createApp() { |
|||
const app = createSSRApp(App) |
|||
app.use(uView) // 使用 uView UI
|
|||
return { |
|||
app |
|||
} |
|||
} |
|||
// #endif
|
@ -0,0 +1,72 @@ |
|||
{ |
|||
"name" : "tall-4-project", |
|||
"appid" : "__UNI__1EC8558", |
|||
"description" : "", |
|||
"versionName" : "1.0.0", |
|||
"versionCode" : "100", |
|||
"transformPx" : false, |
|||
/* 5+App特有相关 */ |
|||
"app-plus" : { |
|||
"usingComponents" : true, |
|||
"nvueStyleCompiler" : "uni-app", |
|||
"compilerVersion" : 3, |
|||
"splashscreen" : { |
|||
"alwaysShowBeforeRender" : true, |
|||
"waiting" : true, |
|||
"autoclose" : true, |
|||
"delay" : 0 |
|||
}, |
|||
/* 模块配置 */ |
|||
"modules" : {}, |
|||
/* 应用发布信息 */ |
|||
"distribute" : { |
|||
/* android打包配置 */ |
|||
"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打包配置 */ |
|||
"ios" : {}, |
|||
/* SDK配置 */ |
|||
"sdkConfigs" : {} |
|||
} |
|||
}, |
|||
/* 快应用特有相关 */ |
|||
"quickapp" : {}, |
|||
/* 小程序特有相关 */ |
|||
"mp-weixin" : { |
|||
"appid" : "", |
|||
"setting" : { |
|||
"urlCheck" : false |
|||
}, |
|||
"usingComponents" : true |
|||
}, |
|||
"mp-alipay" : { |
|||
"usingComponents" : true |
|||
}, |
|||
"mp-baidu" : { |
|||
"usingComponents" : true |
|||
}, |
|||
"mp-toutiao" : { |
|||
"usingComponents" : true |
|||
}, |
|||
"uniStatistics" : { |
|||
"enable" : false |
|||
}, |
|||
"vueVersion" : "3" |
|||
} |
@ -0,0 +1,54 @@ |
|||
{ |
|||
"name": "tall-4", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "main.js", |
|||
"dependencies": {}, |
|||
"devDependencies": { |
|||
"@typescript-eslint/eslint-plugin": "^5.8.1", |
|||
"@typescript-eslint/parser": "^5.8.1", |
|||
"commitizen": "^4.2.4", |
|||
"commitlint": "^16.0.1", |
|||
"conventional-changelog": "^3.1.25", |
|||
"conventional-changelog-cli": "^2.2.2", |
|||
"eslint": "^7.32.0", |
|||
"eslint-config-airbnb-base": "^15.0.0", |
|||
"eslint-plugin-import": "^2.25.3", |
|||
"eslint-plugin-prettier": "^4.0.0", |
|||
"eslint-plugin-vue": "^8.2.0", |
|||
"husky": "^7.0.4", |
|||
"lint-staged": "^12.1.4", |
|||
"prettier": "^2.5.1", |
|||
"right-pad": "^1.0.1", |
|||
"vue-cli-plugin-commitlint": "^1.0.12" |
|||
}, |
|||
"browserslist": [ |
|||
"Android >= 4", |
|||
"ios >= 8" |
|||
], |
|||
"config": { |
|||
"commitizen": { |
|||
"path": "./node_modules/vue-cli-plugin-commitlint/lib/cz" |
|||
} |
|||
}, |
|||
"husky": { |
|||
"hooks": { |
|||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS", |
|||
"pre-commit": "lint-staged" |
|||
} |
|||
}, |
|||
"lint-staged": { |
|||
"src/**/*.{js,json,css,vue}": [ |
|||
"eslint --fix", |
|||
"git add" |
|||
], |
|||
"*.js": "eslint --cache --fix" |
|||
}, |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1", |
|||
"cz": "npm run log && git add . && git cz", |
|||
"log": "conventional-changelog --config ./node_modules/vue-cli-plugin-commitlint/lib/log -i CHANGELOG.md -s -r 0" |
|||
}, |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
@ -0,0 +1,16 @@ |
|||
{ |
|||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages |
|||
{ |
|||
"path": "pages/index/index", |
|||
"style": { |
|||
"navigationBarTitleText": "uni-app" |
|||
} |
|||
} |
|||
], |
|||
"globalStyle": { |
|||
"navigationBarTextStyle": "black", |
|||
"navigationBarTitleText": "uni-app", |
|||
"navigationBarBackgroundColor": "#F8F8F8", |
|||
"backgroundColor": "#F8F8F8" |
|||
} |
|||
} |
@ -0,0 +1,66 @@ |
|||
<template> |
|||
<view :style="{ height: height }" class="flex flex-col overflow-hidden u-font-14"> |
|||
<!-- 标题栏 --> |
|||
<Title /> |
|||
|
|||
<view class="container flex flex-col flex-1 mx-auto overflow-hidden bg-gray-100"> |
|||
<!-- 角色栏 --> |
|||
<Roles /> |
|||
|
|||
<!-- 日常任务面板 --> |
|||
<Globals /> |
|||
|
|||
<!-- 定期任务面板 --> |
|||
<TimeLine @getTasks="getTasks" class="flex-1 overflow-hidden" ref="timeLine" /> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { |
|||
ref, onMounted |
|||
} from 'vue'; |
|||
import Navbar from '@/components/Title/Title.vue'; |
|||
import Roles from '@/components/Roles/Roles.vue'; |
|||
import Globals from '@/components/Globals/Globals.vue'; |
|||
import TimeLine from '@/components/TimeLine/TimeLine.vue'; |
|||
|
|||
let height = ref(null); |
|||
|
|||
onMounted(() => { |
|||
const system = uni.getSystemInfoSync(); |
|||
height.value = system.windowHeight + 'px'; |
|||
}); |
|||
|
|||
function getTasks() { |
|||
|
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.logo { |
|||
height: 200rpx; |
|||
width: 200rpx; |
|||
margin-top: 200rpx; |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
margin-bottom: 50rpx; |
|||
} |
|||
|
|||
.text-area { |
|||
display: flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.title { |
|||
font-size: 36rpx; |
|||
color: #8f8f94; |
|||
} |
|||
</style> |
After Width: | Height: | Size: 3.9 KiB |
@ -0,0 +1,3 @@ |
|||
const actions = {}; |
|||
|
|||
export default actions; |
@ -0,0 +1,3 @@ |
|||
const getters = {}; |
|||
|
|||
export default getters; |
@ -0,0 +1,12 @@ |
|||
import state from './state'; |
|||
import getters from './getters'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}; |
@ -0,0 +1,3 @@ |
|||
const mutations = {}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,7 @@ |
|||
const state = { |
|||
db: null, // indexedDB对象
|
|||
name: 'TALL_indexedDB', |
|||
version: 1, |
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,36 @@ |
|||
import Vue from 'vue'; |
|||
import Vuex from 'vuex'; |
|||
import messages from './messages/index'; |
|||
import project from './project/index'; |
|||
import role from './role/index'; |
|||
import socket from './socket/index'; |
|||
import task from './task/index'; |
|||
import user from './user/index'; |
|||
|
|||
// 不属于具体模块的 应用级的 store内容
|
|||
const state = { |
|||
networkConnected: true, // 网络是否连接
|
|||
forceUseStorage: true, // 强制启用storage
|
|||
}; |
|||
|
|||
const getters = { |
|||
// 是否启用本地存储
|
|||
// 设置了强制启用本地存储 或者 没有网络连接的时候
|
|||
useStorage({ networkConnected, forceUseStorage }) { |
|||
return forceUseStorage || !networkConnected; |
|||
}, |
|||
}; |
|||
|
|||
const mutations = { |
|||
/** |
|||
* 设置网络是否连接的变量 |
|||
* @param {*} state |
|||
* @param {boolean} networkConnected |
|||
*/ |
|||
setNetworkConnected(state, networkConnected) { |
|||
state.networkConnected = networkConnected; |
|||
}, |
|||
}; |
|||
|
|||
Vue.use(Vuex); |
|||
export default new Vuex.Store({ state, getters, mutations, modules: { user, messages, socket, project, role, task } }); |
@ -0,0 +1,3 @@ |
|||
const getters = {}; |
|||
|
|||
export default getters; |
@ -0,0 +1,4 @@ |
|||
import state from './state'; |
|||
import mutations from './mutations'; |
|||
|
|||
export default { namespaced: true, state, mutations }; |
@ -0,0 +1,85 @@ |
|||
import storage from '@/utils/storage'; |
|||
const { setStorageSync, getStorageSync, removeStorageSync } = storage; |
|||
|
|||
const mutations = { |
|||
/** |
|||
* 初始化消息栈 |
|||
* @param {object} state |
|||
* @param {string} type |
|||
* type: |
|||
* syncMessages 同步消息栈 |
|||
* faultMessages 故障消息 未处理消息栈 |
|||
* faults 所有的故障消息栈 |
|||
*/ |
|||
messagesInit(state, type) { |
|||
const messages = getStorageSync(type) ? JSON.parse(getStorageSync(type)) : []; |
|||
state[type] = messages; |
|||
}, |
|||
|
|||
/** |
|||
* 将新 消息添加到 消息栈 最前边 |
|||
* @param { object } state |
|||
* @param { object } data |
|||
* data: message, type |
|||
* message 消息对象 |
|||
* type: |
|||
* syncMessages 同步消息栈 |
|||
* faultMessages 故障消息 未处理消息栈 |
|||
* faults 所有的故障消息栈 |
|||
* game 游戏的消息 |
|||
* |
|||
* cache: boolean true 本地存储 false 不存储 |
|||
*/ |
|||
messagesAdd(state, data) { |
|||
const messages = state[data.type]; |
|||
if (messages.length > 0) { |
|||
const result = messages.find(msg => msg.id === data.message.id); |
|||
if (result) return; |
|||
} |
|||
messages.unshift(data.message); |
|||
// eslint-disable-next-line no-param-reassign
|
|||
state[data.type] = messages; |
|||
if (data.cache) { |
|||
setStorageSync(data.type, JSON.stringify(messages)); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 通过消息id移除指定 同步 消息 |
|||
* @param { object } state |
|||
* @param { object } data |
|||
* data: messageId, type |
|||
* messageId: 要移除的消息的messageId |
|||
* type: |
|||
* syncMessages 同步消息栈 |
|||
* faultMessages 故障消息 未处理消息栈 |
|||
* faults 所有的故障消息栈 |
|||
* cache: boolean true 本地存储 false 不存储 |
|||
*/ |
|||
messagesRemoveById(state, data) { |
|||
const messages = state[data.type]; |
|||
const index = messages.findIndex(msg => msg.id === data.messageId); |
|||
if (index < 0) return; |
|||
messages.splice(index, 1); |
|||
// eslint-disable-next-line no-param-reassign
|
|||
state[data.type] = messages; |
|||
if (data.cache) { |
|||
setStorageSync(data.type, JSON.stringify(messages)); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 清除指定type的消息 |
|||
* @param {any} state |
|||
* @param {object} data |
|||
* data: type, cache |
|||
*/ |
|||
messagesClear(state, data) { |
|||
state[data.type] = []; |
|||
if (data.cache) { |
|||
removeStorageSync(data.type); |
|||
} |
|||
}, |
|||
}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,8 @@ |
|||
const state = { |
|||
syncMessages: [], // 同步消息
|
|||
faultMessages: [], // 新收到的未处理的 故障消息
|
|||
faults: [], // 所有的故障消息
|
|||
game: [], // 游戏的消息
|
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,3 @@ |
|||
const actions = {}; |
|||
|
|||
export default actions; |
@ -0,0 +1,12 @@ |
|||
const getters = { |
|||
/** |
|||
* 当前项目的id |
|||
* @param {object} project |
|||
*/ |
|||
projectId({ project }) { |
|||
uni.$t.storage.setStorageSync('projectId', project.id); |
|||
return project.id; |
|||
}, |
|||
}; |
|||
|
|||
export default getters; |
@ -0,0 +1,12 @@ |
|||
import state from './state'; |
|||
import getters from './getters'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}; |
@ -0,0 +1,43 @@ |
|||
const mutations = { |
|||
/** |
|||
* 设置state projects书籍 |
|||
* @param {object} state |
|||
* @param {array} projects 项目列表 |
|||
*/ |
|||
setProjects(state, projects) { |
|||
if (!projects || !projects.length) { |
|||
state.projects = []; |
|||
} else { |
|||
state.projects = [...projects]; |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 设置当前项目信息 |
|||
* @param { object } state |
|||
* @param { object } data |
|||
*/ |
|||
setProject(state, data) { |
|||
state.project = data || { name: '加载中...' }; |
|||
}, |
|||
|
|||
/** |
|||
* 设置当前项目名称 |
|||
* @param { object } state |
|||
* @param { string } data |
|||
*/ |
|||
setProjectName(state, data) { |
|||
state.project.name = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置小红点 |
|||
* @param { object } state |
|||
* @param { string } data |
|||
*/ |
|||
setDotList(state, data) { |
|||
state.dotList = data; |
|||
}, |
|||
}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,8 @@ |
|||
/* eslint-disable */ |
|||
const state = { |
|||
project: { name: '加载中...' }, // 当前项目信息
|
|||
projects: [], // 项目列表
|
|||
dotList: [], // 小红点
|
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,17 @@ |
|||
const actions = { |
|||
/** |
|||
* 根据项目id查找所有成员信息 |
|||
* @param {*} commit |
|||
* @param {object} params |
|||
*/ |
|||
async getAllMembers({ commit }, params) { |
|||
try { |
|||
const data = await uni.$u.api.queryChecker(params); |
|||
commit('setMembers', data); |
|||
} catch (error) { |
|||
uni.$t.ui.showToast(error.msg || '成员查询失败'); |
|||
} |
|||
}, |
|||
}; |
|||
|
|||
export default actions; |
@ -0,0 +1,13 @@ |
|||
const getters = { |
|||
// 是不是负责人
|
|||
isMine({ roleId, invisibleRoles, visibleRoles }) { |
|||
if (!visibleRoles || !visibleRoles.length) return false; |
|||
const visible = visibleRoles.find(visible => visible.id === roleId); |
|||
if (visible) return visible.mine; |
|||
const invisible = invisibleRoles.find(invisible => invisible.id === roleId); |
|||
if (invisible) return visible.mine; |
|||
return false; |
|||
}, |
|||
}; |
|||
|
|||
export default getters; |
@ -0,0 +1,12 @@ |
|||
import state from './state'; |
|||
import getters from './getters'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}; |
@ -0,0 +1,39 @@ |
|||
const mutations = { |
|||
/** |
|||
* 设置不展示的角色信息 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setInvisibleRoles(state, data) { |
|||
state.invisibleRoles = data || []; |
|||
}, |
|||
|
|||
/** |
|||
* 设置展示的角色信息 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setVisibleRoles(state, data) { |
|||
state.visibleRoles = data || []; |
|||
}, |
|||
|
|||
/** |
|||
* 设置当前角色信息 |
|||
* @param {Object} state |
|||
* @param {string} roleId 当前正在展示的角色的id |
|||
*/ |
|||
setRoleId(state, roleId) { |
|||
state.roleId = roleId; |
|||
}, |
|||
|
|||
/** |
|||
* 设置项目下所有成员信息 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setMembers(state, data) { |
|||
state.members = data || []; |
|||
}, |
|||
}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,8 @@ |
|||
const state = { |
|||
invisibleRoles: [], // 不展示的角色信息
|
|||
visibleRoles: [], // 展示的角色信息
|
|||
roleId: '', // 当前展示查看的角色id
|
|||
members: [], // 项目下所有成员
|
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,154 @@ |
|||
const WS_BASE_URL = process.env.VUE_APP_MSG_URL; |
|||
|
|||
let prevTime = 0; |
|||
let socketMsgQueue = []; // socket消息队列
|
|||
let sendHeartTimer = null; |
|||
|
|||
const actions = { |
|||
// 初始化socket
|
|||
initSocket({ commit, dispatch, state, rootState }) { |
|||
if (state.lockSocket) return; |
|||
const { token } = rootState.user; |
|||
if (!token) return; |
|||
commit('setLockSocket', true); |
|||
commit('setSocket', uni.connectSocket({ url: WS_BASE_URL, complete: () => {} })); |
|||
dispatch('onSocketOpen'); |
|||
dispatch('onSocketMessage'); |
|||
dispatch('onSocketClose'); |
|||
state.socket.onError(errMsg => console.error(errMsg)); |
|||
commit('setLockSocket', false); |
|||
}, |
|||
|
|||
// 监听ws打开
|
|||
onSocketOpen({ dispatch, commit, state }) { |
|||
// eslint-disable-next-line no-unused-vars
|
|||
state.socket.onOpen(res => { |
|||
// console.log('ws open: ', res);
|
|||
commit('setConnected', true); |
|||
prevTime = Date.now(); |
|||
// this.auth();
|
|||
dispatch('auth'); |
|||
for (let i = 0; i < socketMsgQueue.length; i++) { |
|||
dispatch('sendSocketMessage', socketMsgQueue[i]); |
|||
} |
|||
socketMsgQueue = []; |
|||
}); |
|||
}, |
|||
|
|||
// 监听收到的ws消息
|
|||
onSocketMessage({ dispatch, state }) { |
|||
state.socket.onMessage(res => { |
|||
// console.log('收到消息:', res);
|
|||
prevTime = Date.now(); |
|||
if (!res || !res.data || !JSON.parse(res.data)) return; |
|||
const resData = JSON.parse(res.data); |
|||
const { messageSet, ackId } = resData; |
|||
// 处理消息体对象
|
|||
messageSet.forEach(item => dispatch('handleMessagesData', item)); |
|||
ackId && dispatch('sendSocketMessage', { type: 'Ack', data: { ackId } }); |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* 处理收到的消息内容 |
|||
* @param {object} item 单个消息体对象 |
|||
*/ |
|||
handleMessagesData({ dispatch, commit }, item) { |
|||
const data = JSON.parse(item.data); |
|||
switch (data.type) { |
|||
case 'Sync': // 开始某个节点
|
|||
commit('messages/messagesAdd', { message: data, type: 'syncMessages' }, { root: true }); |
|||
break; |
|||
case 'taskStatus': // 任务状态修改相关消息
|
|||
commit('task/setTaskStatus', data.data, { root: true }); |
|||
break; |
|||
case 'switchoverProject': // 打开新项目消息
|
|||
commit('task/setNewProjectInfo', data.data, { root: true }); |
|||
break; |
|||
// case 'Chrome': // !收到开始游戏的消息
|
|||
// console.log('handleMessagesData', data);
|
|||
// // @ts-ignore
|
|||
// util.openGameApp({
|
|||
// type: data.data.type,
|
|||
// projectId: data.data.projectId,
|
|||
// id: data.data.recordId,
|
|||
// token: rootState.user.token,
|
|||
// });
|
|||
// break;
|
|||
// case 'Deliver': // 交付物相关消息
|
|||
// commit('messages/messagesAdd', { type: 'checkMessages', message: data }, { root: true });
|
|||
// break;
|
|||
case 'ChannelStatus': |
|||
dispatch('handleAuthMessage', data); |
|||
break; |
|||
// case 'switchoverProject': // 康复相关消息
|
|||
// dispatch('home/getProjectById', data.data.projectId, { root: true });
|
|||
// break;
|
|||
// case 'startDrill': // 康复开始训练相关消息
|
|||
// console.log('setStartDrillInfo', data.data);
|
|||
// commit('home/setStartDrillMessages', data.data, { root: true });
|
|||
// break;
|
|||
default: |
|||
break; |
|||
} |
|||
}, |
|||
|
|||
// 发送消息
|
|||
sendSocketMessage({ state }, data) { |
|||
if (state.connected) { |
|||
const msg = JSON.stringify({ toDomain: 'Server', data: JSON.stringify(data) }); |
|||
state.socket.send({ data: msg }); |
|||
} else { |
|||
socketMsgQueue.push(data); |
|||
} |
|||
}, |
|||
|
|||
// 监听关闭事件
|
|||
onSocketClose({ dispatch, commit, state }) { |
|||
// console.log('onSocketClose');
|
|||
state.socket.onClose(() => { |
|||
commit('setConnected', false); |
|||
if (sendHeartTimer) clearInterval(sendHeartTimer); |
|||
setTimeout(() => { |
|||
dispatch('initSocket'); |
|||
}, 300); |
|||
}); |
|||
}, |
|||
|
|||
// websocket发送channelId进行认证
|
|||
auth({ dispatch, rootState }) { |
|||
const { token } = rootState.user; |
|||
if (!token) return; |
|||
const data = { type: 'Auth', data: { token } }; |
|||
dispatch('sendSocketMessage', data); |
|||
}, |
|||
|
|||
// 心跳检测
|
|||
sendHeart({ dispatch, state }) { |
|||
if (sendHeartTimer) clearInterval(sendHeartTimer); |
|||
sendHeartTimer = setInterval(() => { |
|||
if (Date.now() - prevTime >= 15000) { |
|||
dispatch('sendSocketMessage', { type: 'Ping' }); |
|||
if (Date.now() - prevTime >= 20000) { |
|||
state.socket.close(); |
|||
} |
|||
} |
|||
}, 5000); |
|||
}, |
|||
|
|||
/** |
|||
* 处理auth认证返回的ChannelStatus消息 |
|||
* @param {object} data 消息内容对象 |
|||
*/ |
|||
handleAuthMessage({ commit, dispatch }, data) { |
|||
if (data.data.authed) { |
|||
dispatch('sendHeart'); |
|||
} else { |
|||
uni.$u.toast('消息系统认证失败, 请退出重新登录'); |
|||
uni.$t.removeStorageSync('anyringToken'); |
|||
commit('setSocket', null); |
|||
} |
|||
}, |
|||
}; |
|||
|
|||
export default actions; |
@ -0,0 +1,5 @@ |
|||
import state from './state'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { namespaced: true, state, mutations, actions }; |
@ -0,0 +1,26 @@ |
|||
const mutations = { |
|||
// 设置socket实例
|
|||
setSocket(state, socket) { |
|||
state.socket = socket; |
|||
}, |
|||
|
|||
/** |
|||
* 设置socket连接状态 |
|||
* @param {Object} state |
|||
* @param {boolean} connected 是否连接 true -> 连接 |
|||
*/ |
|||
setConnected(state, connected) { |
|||
state.connected = connected; |
|||
}, |
|||
|
|||
/** |
|||
* 设置连接锁 正在连接中 锁上 避免多个连接同时发出 |
|||
* @param {Object} state |
|||
* @param {boolean} lockSocket 是否正在连接的过程中 |
|||
*/ |
|||
setLockSocket(state, lockSocket) { |
|||
state.lockSocket = lockSocket; |
|||
}, |
|||
}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,7 @@ |
|||
const state = { |
|||
socket: null, // websocket实例
|
|||
connected: false, // 是否处于连接状态
|
|||
lockSocket: false, // 是否正在连接状态
|
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,33 @@ |
|||
const actions = { |
|||
/** |
|||
* 根据角色查找永久的日常任务 |
|||
* @param {*} commit |
|||
* @param {string} roleId 角色id |
|||
*/ |
|||
getPermanent({ commit }, param) { |
|||
uni.$t.$q.getPermanent(param, (err, data) => { |
|||
if (err) { |
|||
console.error('err: ', err); |
|||
} else { |
|||
commit('setPermanents', data); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* 根据时间和角色查找日常任务 |
|||
* @param {*} commit |
|||
* @param {object} param 请求参数 roleId, timeNode, timeUnit |
|||
*/ |
|||
getGlobal({ commit }, param) { |
|||
uni.$t.$q.getGlobal(param, (err, data) => { |
|||
if (err) { |
|||
console.error('err: ', err); |
|||
} else { |
|||
commit('setDailyTasks', data); |
|||
} |
|||
}); |
|||
}, |
|||
}; |
|||
|
|||
export default actions; |
@ -0,0 +1,23 @@ |
|||
const getters = { |
|||
// 所有的日常任务 永久 + 可变 日常任务
|
|||
globals({ dailyTasks, permanents }) { |
|||
return [...permanents, ...dailyTasks]; |
|||
}, |
|||
|
|||
unitConfig({ timeUnit }) { |
|||
const target = uni.$t.timeConfig.timeUnits.find(item => item.id === timeUnit); |
|||
return target; |
|||
}, |
|||
|
|||
// 计算任务开始时间的格式
|
|||
startTimeFormat(state, { unitConfig }) { |
|||
return unitConfig.format || 'M月D日 HH:mm'; |
|||
}, |
|||
|
|||
// 计算颗粒度 对应的 dayjs add 的单位
|
|||
timeGranularity(state, { unitConfig }) { |
|||
return unitConfig.granularity; |
|||
}, |
|||
}; |
|||
|
|||
export default getters; |
@ -0,0 +1,12 @@ |
|||
import state from './state'; |
|||
import getters from './getters'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}; |
@ -0,0 +1,238 @@ |
|||
const mutations = { |
|||
/** |
|||
* 记录时间轴向上滚动的距离 |
|||
* @param { object } state |
|||
* @param { number } num |
|||
*/ |
|||
setScrollTop(state, num) { |
|||
state.scrollTop = num; |
|||
}, |
|||
|
|||
/** |
|||
* 记录时间轴向上滚动的距离 |
|||
* @param { object } state |
|||
* @param {string} taskId |
|||
*/ |
|||
setScrollToTaskId(state, taskId) { |
|||
state.scrollToTaskId = taskId; |
|||
}, |
|||
|
|||
/** |
|||
* 设置日常任务当前是否应该处于收缩状态 |
|||
* @param { object } state |
|||
* @param { boolean } data |
|||
*/ |
|||
setShrink(state, data) { |
|||
state.isShrink = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置tip的值 |
|||
* @param {object} state |
|||
* @param {object} data |
|||
*/ |
|||
setTip(state, data) { |
|||
if (!data) return; |
|||
state.tip = { ...data }; |
|||
}, |
|||
|
|||
/** |
|||
* 是否显示tips |
|||
* @param { object } state |
|||
* @param { boolean } show |
|||
*/ |
|||
setTipShow(state, show) { |
|||
state.tip.show = show; |
|||
}, |
|||
|
|||
/** |
|||
* 是否显示tips |
|||
* @param { object } state |
|||
* @param { number } status |
|||
*/ |
|||
setStatus(state, status) { |
|||
state.tip.status = status; |
|||
}, |
|||
|
|||
/** |
|||
* 设置时间基准点 |
|||
* @param { object } state |
|||
* @param { number } data |
|||
*/ |
|||
setTimeNode(state, data) { |
|||
state.timeNode = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置时间颗粒度 |
|||
* @param { object } state |
|||
* @param { number } data |
|||
*/ |
|||
setTimeUnit(state, data) { |
|||
state.timeUnit = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置向上查到的定期任务数据 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setUpTasks(state, data) { |
|||
if (!state.tasks.length) { |
|||
state.tasks = [...data]; // 原来没有数据
|
|||
} else { |
|||
state.tasks = [...data, ...state.tasks]; |
|||
|
|||
let arr = [], |
|||
flag = false; |
|||
state.tasks.forEach(task => { |
|||
arr.forEach(item => { |
|||
if (task.id == item.id) { |
|||
flag = true; |
|||
} |
|||
}); |
|||
|
|||
if (!flag) { |
|||
arr.push(task); |
|||
} |
|||
}); |
|||
|
|||
state.tasks = [...arr]; |
|||
// state.tasks = [...data.concat(state.tasks)];
|
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 设置向下查到的定期任务数据 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setDownTasks(state, data) { |
|||
if (!state.tasks && !state.tasks.length) { |
|||
state.tasks = [...data]; |
|||
} else { |
|||
state.tasks = [...state.tasks, ...data]; |
|||
|
|||
let arr = [], |
|||
flag = false; |
|||
state.tasks.forEach(task => { |
|||
arr.forEach(item => { |
|||
if (task.id == item.id) { |
|||
flag = true; |
|||
} |
|||
}); |
|||
|
|||
if (!flag) { |
|||
arr.push(task); |
|||
} |
|||
}); |
|||
|
|||
state.tasks = [...arr]; |
|||
// state.tasks = [...state.tasks.concat(data)];
|
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 添加任务后更新tasks |
|||
* @param {Object} state |
|||
* @param {Array} data 新添加的task |
|||
*/ |
|||
updateTasks(state, data) { |
|||
state.tasks = [...data]; |
|||
}, |
|||
|
|||
/** |
|||
* 设置添加任务的位置 |
|||
* @param {*} state |
|||
* @param {*} data |
|||
*/ |
|||
setAddPosition(state, data) { |
|||
console.log('data: ', data); |
|||
}, |
|||
|
|||
/** |
|||
* 设置日常任务数据 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setDailyTasks(state, data) { |
|||
state.dailyTasks = data || []; |
|||
}, |
|||
|
|||
/** |
|||
* 设置永久固定任务 |
|||
* @param {object} state |
|||
* @param {array} tasks 服务端查询到的永久日常任务书籍 |
|||
*/ |
|||
setPermanents(state, tasks) { |
|||
state.permanents = tasks || []; |
|||
}, |
|||
|
|||
/** |
|||
* 设置时间轴是否继续向上查任务 |
|||
* @param {Object} state |
|||
* @param {Boolean} show |
|||
*/ |
|||
setTopEnd(state, show) { |
|||
state.topEnd = show; |
|||
}, |
|||
|
|||
/** |
|||
* 设置时间轴是否继续向下查任务 |
|||
* @param {Object} state |
|||
* @param {Boolean} show |
|||
*/ |
|||
setBottomEnd(state, show) { |
|||
state.bottomEnd = show; |
|||
}, |
|||
|
|||
// 清空标志位 如切换角色等使用
|
|||
clearEndFlag(state) { |
|||
state.topEnd = false; |
|||
state.bottomEnd = false; |
|||
}, |
|||
|
|||
// 清空定期任务
|
|||
clearTasks(state) { |
|||
state.tasks = []; |
|||
}, |
|||
|
|||
/** |
|||
* 收到消息设置任务状态 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setTaskStatus(state, data) { |
|||
const item = state.tasks.find(i => i.id === data.id); |
|||
item.process = data.taskStatus; |
|||
}, |
|||
|
|||
/** |
|||
* 收到打开新项目消息状态 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setNewProjectInfo(state, data) { |
|||
state.newProjectInfo = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置骨架屏是否显示 |
|||
* @param {Object} state |
|||
* @param {Boolean} show |
|||
*/ |
|||
setShowSkeleton(state, show) { |
|||
state.showSkeleton = show; |
|||
}, |
|||
|
|||
/** |
|||
* 是否设置时间轴自动滚动的位置 |
|||
* @param {Object} state |
|||
* @param {Boolean} show |
|||
*/ |
|||
setShowScrollTo(state, show) { |
|||
state.showScrollTo = show; |
|||
}, |
|||
}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,25 @@ |
|||
const state = { |
|||
scrollTop: 0, |
|||
scrollToTaskId: '', // 时间轴自动滚动的位置
|
|||
isShrink: false, // true: 收起, false:展开
|
|||
tip: { |
|||
taskId: '', // 当前正在修改状态的任务的id
|
|||
show: false, |
|||
status: 0, // 所点击任务的当前状态码
|
|||
text: '', |
|||
left: 0, // 鼠标点击位置距离左边的距离
|
|||
top: 0, // 鼠标点击位置距离上边的距离
|
|||
}, |
|||
timeNode: new Date().getTime(), // 时间基准点
|
|||
timeUnit: 4, // 时间颗粒度
|
|||
topEnd: false, // 时间轴向上查任务到顶了
|
|||
bottomEnd: false, // 时间轴向下查任务到底了
|
|||
permanents: [], // 永久日常任务
|
|||
dailyTasks: [], // 日常任务
|
|||
tasks: [], // 所有的定期任务
|
|||
showSkeleton: false, // 定期任务骨架屏
|
|||
newProjectInfo: {}, |
|||
showScrollTo: false, // 是否可以设置时间轴自动滚动的位置
|
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,19 @@ |
|||
const actions = { |
|||
/** |
|||
* 通过userId获取token |
|||
* @param {any} commit |
|||
* @param {string} userId 用户id |
|||
*/ |
|||
async getToken({ commit }, userId) { |
|||
try { |
|||
const data = await uni.$u.api.getToken(userId); |
|||
commit('setToken', data.token); |
|||
commit('setUser', data); |
|||
return data; |
|||
} catch (error) { |
|||
uni.$t.ui.showToast(error.msg || '获取个人信息失败'); |
|||
} |
|||
}, |
|||
}; |
|||
|
|||
export default actions; |
@ -0,0 +1,14 @@ |
|||
const getters = { |
|||
// 获取用户的id
|
|||
userId({ user }) { |
|||
try { |
|||
if (!user) return ''; |
|||
return user.id; |
|||
} catch (error) { |
|||
console.warn("user's getters 获取userId失败", error); |
|||
return ''; |
|||
} |
|||
}, |
|||
}; |
|||
|
|||
export default getters; |
@ -0,0 +1,12 @@ |
|||
import state from './state'; |
|||
import getters from './getters'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}; |
@ -0,0 +1,24 @@ |
|||
const mutations = { |
|||
/** |
|||
* 设置存储token |
|||
* @param {object} state |
|||
* @param {string} token |
|||
*/ |
|||
setToken(state, token) { |
|||
state.token = token || ''; |
|||
uni.$t.storage.setStorageSync(uni.$t.app.tokenKey, token || ''); |
|||
}, |
|||
|
|||
/** |
|||
* 设置user数据 |
|||
* @param {object} state |
|||
* @param {object} user |
|||
*/ |
|||
setUser(state, user) { |
|||
if (!user) return; |
|||
state.user = { ...user }; |
|||
uni.$t.storage.setStorageSync('user', JSON.stringify(user)); |
|||
}, |
|||
}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,5 @@ |
|||
const state = { |
|||
token: '', |
|||
user: null, |
|||
}; |
|||
export default state; |
@ -0,0 +1,76 @@ |
|||
/** |
|||
* 这里是uni-app内置的常用样式变量 |
|||
* |
|||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 |
|||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App |
|||
* |
|||
*/ |
|||
|
|||
/** |
|||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 |
|||
* |
|||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 |
|||
*/ |
|||
@import "@/uni_modules/vk-uview-ui/theme.scss"; |
|||
/* 颜色变量 */ |
|||
|
|||
/* 行为相关颜色 */ |
|||
$uni-color-primary: #007aff; |
|||
$uni-color-success: #4cd964; |
|||
$uni-color-warning: #f0ad4e; |
|||
$uni-color-error: #dd524d; |
|||
|
|||
/* 文字基本颜色 */ |
|||
$uni-text-color:#333;//基本色 |
|||
$uni-text-color-inverse:#fff;//反色 |
|||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 |
|||
$uni-text-color-placeholder: #808080; |
|||
$uni-text-color-disable:#c0c0c0; |
|||
|
|||
/* 背景颜色 */ |
|||
$uni-bg-color:#ffffff; |
|||
$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: #555555; // 二级标题颜色 |
|||
$uni-font-size-subtitle:26px; |
|||
$uni-color-paragraph: #3F536E; // 文章段落颜色 |
|||
$uni-font-size-paragraph:15px; |
@ -0,0 +1,10 @@ |
|||
## 1.0.3(2021-12-20) |
|||
【优化】u-icon在微信小程序下可能会显示null字符串的问题 |
|||
## 1.0.2(2021-12-09) |
|||
* 1、【优化】`u-button` 组件新增 `timerId` 属性 |
|||
* 之前的效果是:所有按钮一定时间内只能点击1次(`共用计算时间`)导致点击按钮A后无法马上点击按钮B |
|||
* 优化的效果是:每个按钮一定时间内只能点击1次(`分开计算时间`)且支持设置相同的 timerId 来达到指定按钮 `共用计算时间` |
|||
## 1.0.1(2021-11-22) |
|||
* 修复 u-parse 组件在微信小程序上的显示问题。 |
|||
## 1.0.0(2021-11-18) |
|||
uView Vue3.0 横空出世,继承uView1.0意志,再战江湖,风云再起!by vk 2021-11-18 |
@ -0,0 +1,219 @@ |
|||
<template> |
|||
<u-popup mode="bottom" :border-radius="borderRadius" :popup="false" v-model="popupValue" :maskCloseAble="maskCloseAble" |
|||
length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :z-index="uZIndex"> |
|||
<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]"> |
|||
{{tips.text}} |
|||
</view> |
|||
<block v-for="(item, index) in list" :key="index"> |
|||
<view |
|||
@touchmove.stop.prevent |
|||
@tap="itemClick(index)" |
|||
:style="[itemStyle(index)]" |
|||
class="u-action-sheet-item u-line-1" |
|||
:class="[index < list.length - 1 ? 'u-border-bottom' : '']" |
|||
:hover-stay-time="150" |
|||
> |
|||
<text>{{item.text}}</text> |
|||
<text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">{{item.subText}}</text> |
|||
</view> |
|||
</block> |
|||
<view class="u-gab" v-if="cancelBtn"> |
|||
</view> |
|||
<view @touchmove.stop.prevent class="u-actionsheet-cancel u-action-sheet-item" hover-class="u-hover-class" |
|||
:hover-stay-time="150" v-if="cancelBtn" @tap="close">{{cancelText}}</view> |
|||
</u-popup> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* actionSheet 操作菜单 |
|||
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。 |
|||
* @tutorial https://www.uviewui.com/components/actionSheet.html |
|||
* @property {Array<Object>} list 按钮的文字数组,见官方文档示例 |
|||
* @property {Object} tips 顶部的提示文字,见官方文档示例 |
|||
* @property {String} cancel-text 取消按钮的提示文字 |
|||
* @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true) |
|||
* @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0) |
|||
* @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true) |
|||
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false) |
|||
* @property {Number String} z-index z-index值(默认1075) |
|||
* @property {String} cancel-text 取消按钮的提示文字 |
|||
* @event {Function} click 点击ActionSheet列表项时触发 |
|||
* @event {Function} close 点击取消按钮时触发 |
|||
* @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet> |
|||
*/ |
|||
export default { |
|||
name: "u-action-sheet", |
|||
emits: ["update:modelValue", "input", "click", "close"], |
|||
props: { |
|||
// 通过双向绑定控制组件的弹出与收起 |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
modelValue: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 点击遮罩是否可以关闭actionsheet |
|||
maskCloseAble: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx |
|||
list: { |
|||
type: Array, |
|||
default () { |
|||
// 如下 |
|||
// return [{ |
|||
// text: '确定', |
|||
// color: '', |
|||
// fontSize: '' |
|||
// }] |
|||
return []; |
|||
} |
|||
}, |
|||
// 顶部的提示文字 |
|||
tips: { |
|||
type: Object, |
|||
default () { |
|||
return { |
|||
text: '', |
|||
color: '', |
|||
fontSize: '26' |
|||
} |
|||
} |
|||
}, |
|||
// 底部的取消按钮 |
|||
cancelBtn: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距 |
|||
safeAreaInsetBottom: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 弹出的顶部圆角值 |
|||
borderRadius: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 弹出的z-index值 |
|||
zIndex: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 取消按钮的文字提示 |
|||
cancelText: { |
|||
type: String, |
|||
default: '取消' |
|||
} |
|||
}, |
|||
computed: { |
|||
// 顶部提示的样式 |
|||
tipsStyle() { |
|||
let style = {}; |
|||
if (this.tips.color) style.color = this.tips.color; |
|||
if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx'; |
|||
return style; |
|||
}, |
|||
// 操作项目的样式 |
|||
itemStyle() { |
|||
return (index) => { |
|||
let style = {}; |
|||
if (this.list[index].color) style.color = this.list[index].color; |
|||
if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx'; |
|||
// 选项被禁用的样式 |
|||
if (this.list[index].disabled) style.color = '#c0c4cc'; |
|||
return style; |
|||
} |
|||
}, |
|||
uZIndex() { |
|||
// 如果用户有传递z-index值,优先使用 |
|||
return this.zIndex ? this.zIndex : this.$u.zIndex.popup; |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
popupValue:false |
|||
}; |
|||
}, |
|||
watch: { |
|||
value(v1, v2) { |
|||
this.popupValue = v1; |
|||
}, |
|||
modelValue(v1, v2) { |
|||
this.popupValue = v1; |
|||
}, |
|||
}, |
|||
methods: { |
|||
getValue(){ |
|||
// #ifndef VUE3 |
|||
return this.value; |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
return this.modelValue; |
|||
// #endif |
|||
}, |
|||
// 点击取消按钮 |
|||
close() { |
|||
// 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数 |
|||
// 这是一个vue发送事件的特殊用法 |
|||
this.popupClose(); |
|||
this.$emit('close'); |
|||
}, |
|||
// 弹窗关闭 |
|||
popupClose() { |
|||
this.$emit('input', false); |
|||
this.$emit("update:modelValue", false); |
|||
}, |
|||
// 点击某一个item |
|||
itemClick(index) { |
|||
// disabled的项禁止点击 |
|||
if(this.list[index].disabled) return; |
|||
this.$emit('click', index); |
|||
this.$emit('input', false); |
|||
this.$emit("update:modelValue", false); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-tips { |
|||
font-size: 26rpx; |
|||
text-align: center; |
|||
padding: 34rpx 0; |
|||
line-height: 1; |
|||
color: $u-tips-color; |
|||
} |
|||
|
|||
.u-action-sheet-item { |
|||
@include vue-flex;; |
|||
line-height: 1; |
|||
justify-content: center; |
|||
align-items: center; |
|||
font-size: 32rpx; |
|||
padding: 34rpx 0; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.u-action-sheet-item__subtext { |
|||
font-size: 24rpx; |
|||
color: $u-tips-color; |
|||
margin-top: 20rpx; |
|||
} |
|||
|
|||
.u-gab { |
|||
height: 12rpx; |
|||
background-color: rgb(234, 234, 236); |
|||
} |
|||
|
|||
.u-actionsheet-cancel { |
|||
color: $u-main-color; |
|||
} |
|||
</style> |
@ -0,0 +1,257 @@ |
|||
<template> |
|||
<view class="u-alert-tips" v-if="show" :class="[ |
|||
!show ? 'u-close-alert-tips': '', |
|||
type ? 'u-alert-tips--bg--' + type + '-light' : '', |
|||
type ? 'u-alert-tips--border--' + type + '-disabled' : '', |
|||
]" :style="{ |
|||
backgroundColor: bgColor, |
|||
borderColor: borderColor |
|||
}"> |
|||
<view class="u-icon-wrap"> |
|||
<u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon> |
|||
</view> |
|||
<view class="u-alert-content" @tap.stop="click"> |
|||
<view class="u-alert-title" :style="[uTitleStyle]"> |
|||
{{title}} |
|||
</view> |
|||
<view v-if="description" class="u-alert-desc" :style="[descStyle]"> |
|||
{{description}} |
|||
</view> |
|||
</view> |
|||
<view class="u-icon-wrap"> |
|||
<u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc" |
|||
:size="22" class="u-close-icon" :style="{ |
|||
top: description ? '18rpx' : '24rpx' |
|||
}"></u-icon> |
|||
</view> |
|||
<text v-if="closeAble && closeText" class="u-close-text" :style="{ |
|||
top: description ? '18rpx' : '24rpx' |
|||
}">{{closeText}}</text> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* alertTips 警告提示 |
|||
* @description 警告提示,展现需要关注的信息 |
|||
* @tutorial https://uviewui.com/components/alertTips.html |
|||
* @property {String} title 显示的标题文字 |
|||
* @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选 |
|||
* @property {String} type 关闭按钮(默认为叉号icon图标) |
|||
* @property {String} icon 图标名称 |
|||
* @property {Object} icon-style 图标的样式,对象形式 |
|||
* @property {Object} title-style 标题的样式,对象形式 |
|||
* @property {Object} desc-style 描述的样式,对象形式 |
|||
* @property {String} close-able 用文字替代关闭图标,close-able为true时有效 |
|||
* @property {Boolean} show-icon 是否显示左边的辅助图标 |
|||
* @property {Boolean} show 显示或隐藏组件 |
|||
* @event {Function} click 点击组件时触发 |
|||
* @event {Function} close 点击关闭按钮时触发 |
|||
*/ |
|||
export default { |
|||
name: 'u-alert-tips', |
|||
emits: ["click", "close"], |
|||
props: { |
|||
// 显示文字 |
|||
title: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 主题,success/warning/info/error |
|||
type: { |
|||
type: String, |
|||
default: 'warning' |
|||
}, |
|||
// 辅助性文字 |
|||
description: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否可关闭 |
|||
closeAble: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 关闭按钮自定义文本 |
|||
closeText: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否显示图标 |
|||
showIcon: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 文字颜色,如果定义了color值,icon会失效 |
|||
color: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 边框颜色 |
|||
borderColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否显示 |
|||
show: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 左边显示的icon |
|||
icon: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// icon的样式 |
|||
iconStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
// 标题的样式 |
|||
titleStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
// 描述文字的样式 |
|||
descStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
} |
|||
}, |
|||
computed: { |
|||
uTitleStyle() { |
|||
let style = {}; |
|||
// 如果有描述文字的话,标题进行加粗 |
|||
style.fontWeight = this.description ? 500 : 'normal'; |
|||
// 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖 |
|||
return this.$u.deepMerge(style, this.titleStyle); |
|||
}, |
|||
uIcon() { |
|||
// 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标 |
|||
return this.icon ? this.icon : this.$u.type2icon(this.type); |
|||
}, |
|||
uIconType() { |
|||
// 如果有设置图标的样式,优先使用,没有的话,则用type的样式 |
|||
return Object.keys(this.iconStyle).length ? '' : this.type; |
|||
} |
|||
}, |
|||
methods: { |
|||
// 点击内容 |
|||
click() { |
|||
this.$emit('click'); |
|||
}, |
|||
// 点击关闭按钮 |
|||
close() { |
|||
this.$emit('close'); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-alert-tips { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
padding: 16rpx 30rpx; |
|||
border-radius: 8rpx; |
|||
position: relative; |
|||
transition: all 0.3s linear; |
|||
border: 1px solid #fff; |
|||
|
|||
&--bg--primary-light { |
|||
background-color: $u-type-primary-light; |
|||
} |
|||
|
|||
&--bg--info-light { |
|||
background-color: $u-type-info-light; |
|||
} |
|||
|
|||
&--bg--success-light { |
|||
background-color: $u-type-success-light; |
|||
} |
|||
|
|||
&--bg--warning-light { |
|||
background-color: $u-type-warning-light; |
|||
} |
|||
|
|||
&--bg--error-light { |
|||
background-color: $u-type-error-light; |
|||
} |
|||
|
|||
&--border--primary-disabled { |
|||
border-color: $u-type-primary-disabled; |
|||
} |
|||
|
|||
&--border--success-disabled { |
|||
border-color: $u-type-success-disabled; |
|||
} |
|||
|
|||
&--border--error-disabled { |
|||
border-color: $u-type-error-disabled; |
|||
} |
|||
|
|||
&--border--warning-disabled { |
|||
border-color: $u-type-warning-disabled; |
|||
} |
|||
|
|||
&--border--info-disabled { |
|||
border-color: $u-type-info-disabled; |
|||
} |
|||
} |
|||
|
|||
.u-close-alert-tips { |
|||
opacity: 0; |
|||
visibility: hidden; |
|||
} |
|||
|
|||
.u-icon { |
|||
margin-right: 16rpx; |
|||
} |
|||
|
|||
.u-alert-title { |
|||
font-size: 28rpx; |
|||
color: $u-main-color; |
|||
} |
|||
|
|||
.u-alert-desc { |
|||
font-size: 26rpx; |
|||
text-align: left; |
|||
color: $u-content-color; |
|||
} |
|||
|
|||
.u-close-icon { |
|||
position: absolute; |
|||
top: 20rpx; |
|||
right: 20rpx; |
|||
} |
|||
|
|||
.u-close-hover { |
|||
color: red; |
|||
} |
|||
|
|||
.u-close-text { |
|||
font-size: 24rpx; |
|||
color: $u-tips-color; |
|||
position: absolute; |
|||
top: 20rpx; |
|||
right: 20rpx; |
|||
line-height: 1; |
|||
} |
|||
</style> |
@ -0,0 +1,290 @@ |
|||
<template> |
|||
<view class="content"> |
|||
<view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }"> |
|||
<canvas |
|||
class="cropper" |
|||
:disable-scroll="true" |
|||
@touchstart="touchStart" |
|||
@touchmove="touchMove" |
|||
@touchend="touchEnd" |
|||
:style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }" |
|||
canvas-id="cropper" |
|||
id="cropper" |
|||
></canvas> |
|||
<canvas |
|||
class="cropper" |
|||
:disable-scroll="true" |
|||
:style="{ |
|||
position: 'fixed', |
|||
top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`, |
|||
left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`, |
|||
width: `${cropperOpt.width * cropperOpt.pixelRatio}px`, |
|||
height: `${cropperOpt.height * cropperOpt.pixelRatio}` |
|||
}" |
|||
canvas-id="targetId" |
|||
id="targetId" |
|||
></canvas> |
|||
</view> |
|||
<view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }"> |
|||
<!-- #ifdef H5 --> |
|||
<view class="upload" @tap="uploadTap">选择图片</view> |
|||
<!-- #endif --> |
|||
<!-- #ifndef H5 --> |
|||
<view class="upload" @tap="uploadTap">重新选择</view> |
|||
<!-- #endif --> |
|||
<view class="getCropperImage" @tap="getCropperImage(false)">确定</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import WeCropper from './weCropper.js'; |
|||
export default { |
|||
props: { |
|||
// 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色, |
|||
// mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)" |
|||
boundStyle: { |
|||
type: Object, |
|||
default() { |
|||
return { |
|||
lineWidth: 4, |
|||
borderColor: 'rgb(245, 245, 245)', |
|||
mask: 'rgba(0, 0, 0, 0.35)' |
|||
}; |
|||
} |
|||
} |
|||
// // 裁剪框宽度,单位rpx |
|||
// rectWidth: { |
|||
// type: [String, Number], |
|||
// default: 400 |
|||
// }, |
|||
// // 裁剪框高度,单位rpx |
|||
// rectHeight: { |
|||
// type: [String, Number], |
|||
// default: 400 |
|||
// }, |
|||
// // 输出图片宽度,单位rpx |
|||
// destWidth: { |
|||
// type: [String, Number], |
|||
// default: 400 |
|||
// }, |
|||
// // 输出图片高度,单位rpx |
|||
// destHeight: { |
|||
// type: [String, Number], |
|||
// default: 400 |
|||
// }, |
|||
// // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可 |
|||
// fileType: { |
|||
// type: String, |
|||
// default: 'jpg', |
|||
// }, |
|||
// // 生成的图片质量 |
|||
// // H5上无效,目前不考虑使用此参数 |
|||
// quality: { |
|||
// type: [Number, String], |
|||
// default: 1 |
|||
// } |
|||
}, |
|||
data() { |
|||
return { |
|||
// 底部导航的高度 |
|||
bottomNavHeight: 50, |
|||
originWidth: 200, |
|||
width: 0, |
|||
height: 0, |
|||
cropperOpt: { |
|||
id: 'cropper', |
|||
targetId: 'targetCropper', |
|||
pixelRatio: 1, |
|||
width: 0, |
|||
height: 0, |
|||
scale: 2.5, |
|||
zoom: 8, |
|||
cut: { |
|||
x: (this.width - this.originWidth) / 2, |
|||
y: (this.height - this.originWidth) / 2, |
|||
width: this.originWidth, |
|||
height: this.originWidth |
|||
}, |
|||
boundStyle: { |
|||
lineWidth: uni.upx2px(this.boundStyle.lineWidth), |
|||
mask: this.boundStyle.mask, |
|||
color: this.boundStyle.borderColor |
|||
} |
|||
}, |
|||
// 裁剪框和输出图片的尺寸,高度默认等于宽度 |
|||
// 输出图片宽度,单位px |
|||
destWidth: 200, |
|||
// 裁剪框宽度,单位px |
|||
rectWidth: 200, |
|||
// 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可 |
|||
fileType: 'jpg', |
|||
src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片 |
|||
}; |
|||
}, |
|||
onLoad(option) { |
|||
let rectInfo = uni.getSystemInfoSync(); |
|||
this.width = rectInfo.windowWidth; |
|||
this.height = rectInfo.windowHeight - this.bottomNavHeight; |
|||
this.cropperOpt.width = this.width; |
|||
this.cropperOpt.height = this.height; |
|||
this.cropperOpt.pixelRatio = rectInfo.pixelRatio; |
|||
|
|||
if (option.destWidth) this.destWidth = option.destWidth; |
|||
if (option.rectWidth) { |
|||
let rectWidth = Number(option.rectWidth); |
|||
this.cropperOpt.cut = { |
|||
x: (this.width - rectWidth) / 2, |
|||
y: (this.height - rectWidth) / 2, |
|||
width: rectWidth, |
|||
height: rectWidth |
|||
}; |
|||
} |
|||
this.rectWidth = option.rectWidth; |
|||
if (option.fileType) this.fileType = option.fileType; |
|||
// 初始化 |
|||
this.cropper = new WeCropper(this.cropperOpt) |
|||
.on('ready', ctx => { |
|||
// wecropper is ready for work! |
|||
}) |
|||
.on('beforeImageLoad', ctx => { |
|||
// before picture loaded, i can do something |
|||
}) |
|||
.on('imageLoad', ctx => { |
|||
// picture loaded |
|||
}) |
|||
.on('beforeDraw', (ctx, instance) => { |
|||
// before canvas draw,i can do something |
|||
}); |
|||
// 设置导航栏样式,以免用户在page.json中没有设置为黑色背景 |
|||
uni.setNavigationBarColor({ |
|||
frontColor: '#ffffff', |
|||
backgroundColor: '#000000' |
|||
}); |
|||
uni.chooseImage({ |
|||
count: 1, // 默认9 |
|||
sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有 |
|||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 |
|||
success: res => { |
|||
this.src = res.tempFilePaths[0]; |
|||
// 获取裁剪图片资源后,给data添加src属性及其值 |
|||
this.cropper.pushOrign(this.src); |
|||
} |
|||
}); |
|||
}, |
|||
methods: { |
|||
touchStart(e) { |
|||
this.cropper.touchStart(e); |
|||
}, |
|||
touchMove(e) { |
|||
this.cropper.touchMove(e); |
|||
}, |
|||
touchEnd(e) { |
|||
this.cropper.touchEnd(e); |
|||
}, |
|||
getCropperImage(isPre = false) { |
|||
if(!this.src) return this.$u.toast('请先选择图片再裁剪'); |
|||
|
|||
let cropper_opt = { |
|||
destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值 |
|||
destWidth: Number(this.destWidth), |
|||
fileType: this.fileType |
|||
}; |
|||
this.cropper.getCropperImage(cropper_opt, (path, err) => { |
|||
if (err) { |
|||
uni.showModal({ |
|||
title: '温馨提示', |
|||
content: err.message |
|||
}); |
|||
} else { |
|||
if (isPre) { |
|||
uni.previewImage({ |
|||
current: '', // 当前显示图片的 http 链接 |
|||
urls: [path] // 需要预览的图片 http 链接列表 |
|||
}); |
|||
} else { |
|||
uni.$emit('uAvatarCropper', path); |
|||
this.$u.route({ |
|||
type: 'back' |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
uploadTap() { |
|||
const self = this; |
|||
uni.chooseImage({ |
|||
count: 1, // 默认9 |
|||
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有 |
|||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 |
|||
success: (res) => { |
|||
self.src = res.tempFilePaths[0]; |
|||
// 获取裁剪图片资源后,给data添加src属性及其值 |
|||
|
|||
self.cropper.pushOrign(this.src); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import '../../libs/css/style.components.scss'; |
|||
|
|||
.content { |
|||
background: rgba(255, 255, 255, 1); |
|||
} |
|||
|
|||
.cropper { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
z-index: 11; |
|||
} |
|||
|
|||
.cropper-buttons { |
|||
background-color: #000000; |
|||
color: #eee; |
|||
} |
|||
|
|||
.cropper-wrapper { |
|||
position: relative; |
|||
@include vue-flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
width: 100%; |
|||
background-color: #000; |
|||
} |
|||
|
|||
.cropper-buttons { |
|||
width: 100vw; |
|||
@include vue-flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.cropper-buttons .upload, |
|||
.cropper-buttons .getCropperImage { |
|||
width: 50%; |
|||
text-align: center; |
|||
} |
|||
|
|||
.cropper-buttons .upload { |
|||
text-align: left; |
|||
padding-left: 50rpx; |
|||
} |
|||
|
|||
.cropper-buttons .getCropperImage { |
|||
text-align: right; |
|||
padding-right: 50rpx; |
|||
} |
|||
</style> |
File diff suppressed because it is too large
@ -0,0 +1,263 @@ |
|||
<template> |
|||
<view class="u-avatar" :style="[wrapStyle]" @tap="click"> |
|||
<image |
|||
@error="loadError" |
|||
:style="[imgStyle]" |
|||
class="u-avatar__img" |
|||
v-if="!uText && avatar" |
|||
:src="avatar" |
|||
:mode="imgMode" |
|||
></image> |
|||
<text |
|||
class="u-line-1" |
|||
v-else-if="uText" |
|||
:style="{ |
|||
fontSize: '38rpx' |
|||
}" |
|||
> |
|||
{{ uText }} |
|||
</text> |
|||
<slot v-else></slot> |
|||
<view |
|||
class="u-avatar__sex" |
|||
v-if="showSex" |
|||
:class="['u-avatar__sex--' + sexIcon]" |
|||
:style="[uSexStyle]" |
|||
> |
|||
<u-icon :name="sexIcon" size="20"></u-icon> |
|||
</view> |
|||
<view class="u-avatar__level" v-if="showLevel" :style="[uLevelStyle]"> |
|||
<u-icon :name="levelIcon" size="20"></u-icon> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
let base64Avatar = |
|||
"data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z"; |
|||
/** |
|||
* avatar 头像 |
|||
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。 |
|||
* @tutorial https://www.uviewui.com/components/avatar.html |
|||
* @property {String} bg-color 背景颜色,一般显示文字时用(默认#ffffff) |
|||
* @property {String} src 头像路径,如加载失败,将会显示默认头像 |
|||
* @property {String Number} size 头像尺寸,可以为指定字符串(large, default, mini),或者数值,单位rpx(默认default) |
|||
* @property {String} mode 显示类型,见上方说明(默认circle) |
|||
* @property {String} sex-icon 性别图标,man-男,woman-女(默认man) |
|||
* @property {String} level-icon 等级图标(默认level) |
|||
* @property {String} sex-bg-color 性别图标背景颜色 |
|||
* @property {String} level-bg-color 等级图标背景颜色 |
|||
* @property {String} show-sex 是否显示性别图标(默认false) |
|||
* @property {String} show-level 是否显示等级图标(默认false) |
|||
* @property {String} img-mode 头像图片的裁剪类型,与uni的image组件的mode参数一致,如效果达不到需求,可尝试传widthFix值(默认aspectFill) |
|||
* @property {String} index 用户传递的标识符值,如果是列表循环,可穿v-for的index值 |
|||
* @event {Function} click 头像被点击 |
|||
* @example <u-avatar :src="src"></u-avatar> |
|||
*/ |
|||
export default { |
|||
name: "u-avatar", |
|||
emits: ["click"], |
|||
props: { |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: "transparent" |
|||
}, |
|||
// 头像路径 |
|||
src: { |
|||
type: String, |
|||
default: "" |
|||
}, |
|||
// 尺寸,large-大,default-中等,mini-小,如果为数值,则单位为rpx |
|||
// 宽度等于高度 |
|||
size: { |
|||
type: [String, Number], |
|||
default: "default" |
|||
}, |
|||
// 头像模型,square-带圆角方形,circle-圆形 |
|||
mode: { |
|||
type: String, |
|||
default: "circle" |
|||
}, |
|||
// 文字内容 |
|||
text: { |
|||
type: String, |
|||
default: "" |
|||
}, |
|||
// 图片的裁剪模型 |
|||
imgMode: { |
|||
type: String, |
|||
default: "aspectFill" |
|||
}, |
|||
// 标识符 |
|||
index: { |
|||
type: [String, Number], |
|||
default: "" |
|||
}, |
|||
// 右上角性别角标,man-男,woman-女 |
|||
sexIcon: { |
|||
type: String, |
|||
default: "man" |
|||
}, |
|||
// 右下角的等级图标 |
|||
levelIcon: { |
|||
type: String, |
|||
default: "level" |
|||
}, |
|||
// 右下角等级图标背景颜色 |
|||
levelBgColor: { |
|||
type: String, |
|||
default: "" |
|||
}, |
|||
// 右上角性别图标的背景颜色 |
|||
sexBgColor: { |
|||
type: String, |
|||
default: "" |
|||
}, |
|||
// 是否显示性别图标 |
|||
showSex: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否显示等级图标 |
|||
showLevel: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
error: false, |
|||
// 头像的地址,因为如果加载错误,需要赋值为默认图片,props值无法修改,所以需要一个中间值 |
|||
avatar: this.src ? this.src : base64Avatar |
|||
}; |
|||
}, |
|||
watch: { |
|||
src(n) { |
|||
// 用户可能会在头像加载失败时,再次修改头像值,所以需要重新赋值 |
|||
if (!n) { |
|||
// 如果传入null或者'',或者undefined,显示默认头像 |
|||
this.avatar = base64Avatar; |
|||
this.error = true; |
|||
} else { |
|||
this.avatar = n; |
|||
this.error = false; |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
wrapStyle() { |
|||
let style = {}; |
|||
style.height = |
|||
this.size == "large" |
|||
? "120rpx" |
|||
: this.size == "default" |
|||
? "90rpx" |
|||
: this.size == "mini" |
|||
? "70rpx" |
|||
: this.size + "rpx"; |
|||
style.width = style.height; |
|||
style.flex = `0 0 ${style.height}`; |
|||
style.backgroundColor = this.bgColor; |
|||
style.borderRadius = this.mode == "circle" ? "500px" : "5px"; |
|||
if (this.text) style.padding = `0 6rpx`; |
|||
return style; |
|||
}, |
|||
imgStyle() { |
|||
let style = {}; |
|||
style.borderRadius = this.mode == "circle" ? "500px" : "5px"; |
|||
return style; |
|||
}, |
|||
// 取字符串的第一个字符 |
|||
uText() { |
|||
return String(this.text)[0]; |
|||
}, |
|||
// 性别图标的自定义样式 |
|||
uSexStyle() { |
|||
let style = {}; |
|||
if (this.sexBgColor) style.backgroundColor = this.sexBgColor; |
|||
return style; |
|||
}, |
|||
// 等级图标的自定义样式 |
|||
uLevelStyle() { |
|||
let style = {}; |
|||
if (this.levelBgColor) style.backgroundColor = this.levelBgColor; |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
// 图片加载错误时,显示默认头像 |
|||
loadError() { |
|||
this.error = true; |
|||
this.avatar = base64Avatar; |
|||
}, |
|||
click() { |
|||
this.$emit("click", this.index); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-avatar { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 28rpx; |
|||
color: $u-content-color; |
|||
border-radius: 10px; |
|||
position: relative; |
|||
|
|||
&__img { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
&__sex { |
|||
position: absolute; |
|||
width: 32rpx; |
|||
color: #ffffff; |
|||
height: 32rpx; |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
border-radius: 100rpx; |
|||
top: 5%; |
|||
z-index: 1; |
|||
right: -7%; |
|||
border: 1px #ffffff solid; |
|||
|
|||
&--man { |
|||
background-color: $u-type-primary; |
|||
} |
|||
|
|||
&--woman { |
|||
background-color: $u-type-error; |
|||
} |
|||
|
|||
&--none { |
|||
background-color: $u-type-warning; |
|||
} |
|||
} |
|||
|
|||
&__level { |
|||
position: absolute; |
|||
width: 32rpx; |
|||
color: #ffffff; |
|||
height: 32rpx; |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
border-radius: 100rpx; |
|||
bottom: 5%; |
|||
z-index: 1; |
|||
right: -7%; |
|||
border: 1px #ffffff solid; |
|||
background-color: $u-type-warning; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,153 @@ |
|||
<template> |
|||
<view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{ |
|||
bottom: bottom + 'rpx', |
|||
right: right + 'rpx', |
|||
borderRadius: mode == 'circle' ? '10000rpx' : '8rpx', |
|||
zIndex: uZIndex, |
|||
opacity: opacity |
|||
}, customStyle]"> |
|||
<view class="u-back-top__content" v-if="!$slots.default && !$slots.$default"> |
|||
<u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon> |
|||
<view class="u-back-top__content__tips"> |
|||
{{tips}} |
|||
</view> |
|||
</view> |
|||
<slot v-else /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'u-back-top', |
|||
props: { |
|||
// 返回顶部的形状,circle-圆形,square-方形 |
|||
mode: { |
|||
type: String, |
|||
default: 'circle' |
|||
}, |
|||
// 自定义图标 |
|||
icon: { |
|||
type: String, |
|||
default: 'arrow-upward' |
|||
}, |
|||
// 提示文字 |
|||
tips: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 返回顶部滚动时间 |
|||
duration: { |
|||
type: [Number, String], |
|||
default: 100 |
|||
}, |
|||
// 滚动距离 |
|||
scrollTop: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 距离顶部多少距离显示,单位rpx |
|||
top: { |
|||
type: [Number, String], |
|||
default: 400 |
|||
}, |
|||
// 返回顶部按钮到底部的距离,单位rpx |
|||
bottom: { |
|||
type: [Number, String], |
|||
default: 200 |
|||
}, |
|||
// 返回顶部按钮到右边的距离,单位rpx |
|||
right: { |
|||
type: [Number, String], |
|||
default: 40 |
|||
}, |
|||
// 层级 |
|||
zIndex: { |
|||
type: [Number, String], |
|||
default: '9' |
|||
}, |
|||
// 图标的样式,对象形式 |
|||
iconStyle: { |
|||
type: Object, |
|||
default() { |
|||
return { |
|||
color: '#909399', |
|||
fontSize: '38rpx' |
|||
} |
|||
} |
|||
}, |
|||
// 整个组件的样式 |
|||
customStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
} |
|||
}, |
|||
watch: { |
|||
showBackTop(nVal, oVal) { |
|||
// 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度 |
|||
// 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果 |
|||
if(nVal) { |
|||
this.uZIndex = this.zIndex; |
|||
this.opacity = 1; |
|||
} else { |
|||
this.uZIndex = -1; |
|||
this.opacity = 0; |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
showBackTop() { |
|||
// 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值 |
|||
// 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮 |
|||
return this.scrollTop > uni.upx2px(this.top); |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
// 不透明度,为了让组件有一个显示和隐藏的过渡动画 |
|||
opacity: 0, |
|||
// 组件的z-index值,隐藏时设置为-1,就会看不到 |
|||
uZIndex: -1 |
|||
} |
|||
}, |
|||
methods: { |
|||
backToTop() { |
|||
uni.pageScrollTo({ |
|||
scrollTop: 0, |
|||
duration: this.duration |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-back-top { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
position: fixed; |
|||
z-index: 9; |
|||
@include vue-flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
background-color: #E1E1E1; |
|||
color: $u-content-color; |
|||
align-items: center; |
|||
transition: opacity 0.4s; |
|||
|
|||
&__content { |
|||
@include vue-flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
|
|||
&__tips { |
|||
font-size: 24rpx; |
|||
transform: scale(0.8); |
|||
line-height: 1; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,216 @@ |
|||
<template> |
|||
<view v-if="show" class="u-badge" :class="[ |
|||
isDot ? 'u-badge-dot' : '', |
|||
size == 'mini' ? 'u-badge-mini' : '', |
|||
type ? 'u-badge--bg--' + type : '' |
|||
]" :style="[{ |
|||
top: offset[0] + 'rpx', |
|||
right: offset[1] + 'rpx', |
|||
fontSize: fontSize + 'rpx', |
|||
position: absolute ? 'absolute' : 'static', |
|||
color: color, |
|||
backgroundColor: bgColor |
|||
}, boxStyle]" |
|||
> |
|||
{{showText}} |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* badge 角标 |
|||
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。 |
|||
* @tutorial https://www.uviewui.com/components/badge.html |
|||
* @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为0且show-zero为false时隐藏 |
|||
* @property {Boolean} is-dot 不展示数字,只有一个小点(默认false) |
|||
* @property {Boolean} absolute 组件是否绝对定位,为true时,offset参数才有效(默认true) |
|||
* @property {String Number} overflow-count 展示封顶的数字值(默认99) |
|||
* @property {String} type 使用预设的背景颜色(默认error) |
|||
* @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge(默认false) |
|||
* @property {String} size Badge的尺寸,设为mini会得到小一号的Badge(默认default) |
|||
* @property {Array} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx。absolute为true时有效(默认[20, 20]) |
|||
* @property {String} color 字体颜色(默认#ffffff) |
|||
* @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效 |
|||
* @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false) |
|||
* @example <u-badge type="error" count="7"></u-badge> |
|||
*/ |
|||
export default { |
|||
name: 'u-badge', |
|||
props: { |
|||
// primary,warning,success,error,info |
|||
type: { |
|||
type: String, |
|||
default: 'error' |
|||
}, |
|||
// default, mini |
|||
size: { |
|||
type: String, |
|||
default: 'default' |
|||
}, |
|||
//是否是圆点 |
|||
isDot: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 显示的数值内容 |
|||
count: { |
|||
type: [Number, String], |
|||
}, |
|||
// 展示封顶的数字值 |
|||
overflowCount: { |
|||
type: Number, |
|||
default: 99 |
|||
}, |
|||
// 当数值为 0 时,是否展示 Badge |
|||
showZero: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 位置偏移 |
|||
offset: { |
|||
type: Array, |
|||
default: () => { |
|||
return [20, 20] |
|||
} |
|||
}, |
|||
// 是否开启绝对定位,开启了offset才会起作用 |
|||
absolute: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 字体大小 |
|||
fontSize: { |
|||
type: [String, Number], |
|||
default: '24' |
|||
}, |
|||
// 字体演示 |
|||
color: { |
|||
type: String, |
|||
default: '#ffffff' |
|||
}, |
|||
// badge的背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效 |
|||
isCenter: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
computed: { |
|||
// 是否将badge中心与父组件右上角重合 |
|||
boxStyle() { |
|||
let style = {}; |
|||
if(this.isCenter) { |
|||
style.top = 0; |
|||
style.right = 0; |
|||
// Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半 |
|||
style.transform = "translateY(-50%) translateX(50%)"; |
|||
} else { |
|||
style.top = this.offset[0] + 'rpx'; |
|||
style.right = this.offset[1] + 'rpx'; |
|||
style.transform = "translateY(0) translateX(0)"; |
|||
} |
|||
// 如果尺寸为mini,后接上scal() |
|||
if(this.size == 'mini') { |
|||
style.transform = style.transform + " scale(0.8)"; |
|||
} |
|||
return style; |
|||
}, |
|||
// isDot类型时,不显示文字 |
|||
showText() { |
|||
if(this.isDot) return ''; |
|||
else { |
|||
if(this.count > this.overflowCount) return `${this.overflowCount}+`; |
|||
else return this.count; |
|||
} |
|||
}, |
|||
// 是否显示组件 |
|||
show() { |
|||
// 如果count的值为0,并且showZero设置为false,不显示组件 |
|||
if(this.count == 0 && this.showZero == false) return false; |
|||
else return true; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-badge { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
justify-content: center; |
|||
align-items: center; |
|||
line-height: 24rpx; |
|||
padding: 4rpx 8rpx; |
|||
border-radius: 100rpx; |
|||
z-index: 9; |
|||
|
|||
&--bg--primary { |
|||
background-color: $u-type-primary; |
|||
} |
|||
|
|||
&--bg--error { |
|||
background-color: $u-type-error; |
|||
} |
|||
|
|||
&--bg--success { |
|||
background-color: $u-type-success; |
|||
} |
|||
|
|||
&--bg--info { |
|||
background-color: $u-type-info; |
|||
} |
|||
|
|||
&--bg--warning { |
|||
background-color: $u-type-warning; |
|||
} |
|||
} |
|||
|
|||
.u-badge-dot { |
|||
height: 16rpx; |
|||
width: 16rpx; |
|||
border-radius: 100rpx; |
|||
line-height: 1; |
|||
} |
|||
|
|||
.u-badge-mini { |
|||
transform: scale(0.8); |
|||
transform-origin: center center; |
|||
} |
|||
|
|||
// .u-primary { |
|||
// background: $u-type-primary; |
|||
// color: #fff; |
|||
// } |
|||
|
|||
// .u-error { |
|||
// background: $u-type-error; |
|||
// color: #fff; |
|||
// } |
|||
|
|||
// .u-warning { |
|||
// background: $u-type-warning; |
|||
// color: #fff; |
|||
// } |
|||
|
|||
// .u-success { |
|||
// background: $u-type-success; |
|||
// color: #fff; |
|||
// } |
|||
|
|||
// .u-black { |
|||
// background: #585858; |
|||
// color: #fff; |
|||
// } |
|||
|
|||
.u-info { |
|||
background-color: $u-type-info; |
|||
color: #fff; |
|||
} |
|||
</style> |
@ -0,0 +1,602 @@ |
|||
<template> |
|||
<button |
|||
id="u-wave-btn" |
|||
class="u-btn u-line-1 u-fix-ios-appearance" |
|||
:class="[ |
|||
'u-size-' + size, |
|||
plain ? 'u-btn--' + type + '--plain' : '', |
|||
loading ? 'u-loading' : '', |
|||
shape == 'circle' ? 'u-round-circle' : '', |
|||
hairLine ? showHairLineBorder : 'u-btn--bold-border', |
|||
'u-btn--' + type, |
|||
disabled ? `u-btn--${type}--disabled` : '', |
|||
]" |
|||
:hover-start-time="Number(hoverStartTime)" |
|||
:hover-stay-time="Number(hoverStayTime)" |
|||
:disabled="disabled" |
|||
:form-type="formType" |
|||
:open-type="openType" |
|||
:app-parameter="appParameter" |
|||
:hover-stop-propagation="hoverStopPropagation" |
|||
:send-message-title="sendMessageTitle" |
|||
send-message-path="sendMessagePath" |
|||
:lang="lang" |
|||
:data-name="dataName" |
|||
:session-from="sessionFrom" |
|||
:send-message-img="sendMessageImg" |
|||
:show-message-card="showMessageCard" |
|||
@getphonenumber="getphonenumber" |
|||
@getuserinfo="getuserinfo" |
|||
@error="error" |
|||
@opensetting="opensetting" |
|||
@launchapp="launchapp" |
|||
:style="[customStyle, { |
|||
overflow: ripple ? 'hidden' : 'visible' |
|||
}]" |
|||
@tap.stop="click($event)" |
|||
:hover-class="getHoverClass" |
|||
:loading="loading" |
|||
> |
|||
<slot></slot> |
|||
<view |
|||
v-if="ripple" |
|||
class="u-wave-ripple" |
|||
:class="[waveActive ? 'u-wave-active' : '']" |
|||
:style="{ |
|||
top: rippleTop + 'px', |
|||
left: rippleLeft + 'px', |
|||
width: fields.targetWidth + 'px', |
|||
height: fields.targetWidth + 'px', |
|||
'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)' |
|||
}" |
|||
></view> |
|||
</button> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* button 按钮 |
|||
* @description Button 按钮 |
|||
* @tutorial https://www.uviewui.com/components/button.html |
|||
* @property {String} size 按钮的大小 |
|||
* @property {Boolean} ripple 是否开启点击水波纹效果 |
|||
* @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效 |
|||
* @property {String} type 按钮的样式类型 |
|||
* @property {Boolean} plain 按钮是否镂空,背景色透明 |
|||
* @property {Boolean} disabled 是否禁用 |
|||
* @property {Boolean} hair-line 是否显示按钮的细边框(默认true) |
|||
* @property {Boolean} shape 按钮外观形状,见文档说明 |
|||
* @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈) |
|||
* @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件 |
|||
* @property {String} open-type 开放能力 |
|||
* @property {String} data-name 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取 |
|||
* @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持) |
|||
* @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒 |
|||
* @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒 |
|||
* @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明 |
|||
* @event {Function} click 按钮点击 |
|||
* @event {Function} getphonenumber open-type="getPhoneNumber"时有效 |
|||
* @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo |
|||
* @event {Function} error 当使用开放能力时,发生错误的回调 |
|||
* @event {Function} opensetting 在打开授权设置页并关闭后回调 |
|||
* @event {Function} launchapp 打开 APP 成功的回调 |
|||
* @example <u-button>月落</u-button> |
|||
*/ |
|||
export default { |
|||
name: 'u-button', |
|||
emits: ["click", "getphonenumber", "getuserinfo", "error", "opensetting", "launchapp"], |
|||
props: { |
|||
// 是否细边框 |
|||
hairLine: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 按钮的预置样式,default,primary,error,warning,success |
|||
type: { |
|||
type: String, |
|||
default: 'default' |
|||
}, |
|||
// 按钮尺寸,default,medium,mini |
|||
size: { |
|||
type: String, |
|||
default: 'default' |
|||
}, |
|||
// 按钮形状,circle(两边为半圆),square(带圆角) |
|||
shape: { |
|||
type: String, |
|||
default: 'square' |
|||
}, |
|||
// 按钮是否镂空 |
|||
plain: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否禁止状态 |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否加载中 |
|||
loading: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 开放能力,具体请看uniapp稳定关于button组件部分说明 |
|||
// https://uniapp.dcloud.io/component/button |
|||
openType: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件 |
|||
// 取值为submit(提交表单),reset(重置表单) |
|||
formType: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 |
|||
// 只微信小程序、QQ小程序有效 |
|||
appParameter: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效 |
|||
hoverStopPropagation: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效 |
|||
lang: { |
|||
type: String, |
|||
default: 'en' |
|||
}, |
|||
// 会话来源,open-type="contact"时有效。只微信小程序有效 |
|||
sessionFrom: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 会话内消息卡片标题,open-type="contact"时有效 |
|||
// 默认当前标题,只微信小程序有效 |
|||
sendMessageTitle: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效 |
|||
// 默认当前分享路径,只微信小程序有效 |
|||
sendMessagePath: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 会话内消息卡片图片,open-type="contact"时有效 |
|||
// 默认当前页面截图,只微信小程序有效 |
|||
sendMessageImg: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示, |
|||
// 用户点击后可以快速发送小程序消息,open-type="contact"时有效 |
|||
showMessageCard: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 手指按(触摸)按钮时按钮时的背景颜色 |
|||
hoverBgColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 水波纹的背景颜色 |
|||
rippleBgColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否开启水波纹效果 |
|||
ripple: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 按下的类名 |
|||
hoverClass: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 自定义样式,对象形式 |
|||
customStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取 |
|||
dataName: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 节流,一定时间内只能触发一次 |
|||
throttleTime: { |
|||
type: [String, Number], |
|||
default: 500 |
|||
}, |
|||
// 按住后多久出现点击态,单位毫秒 |
|||
hoverStartTime: { |
|||
type: [String, Number], |
|||
default: 20 |
|||
}, |
|||
// 手指松开后点击态保留时间,单位毫秒 |
|||
hoverStayTime: { |
|||
type: [String, Number], |
|||
default: 150 |
|||
}, |
|||
timerId: { |
|||
type: [String, Number] |
|||
}, |
|||
}, |
|||
computed: { |
|||
// 当没有传bgColor变量时,按钮按下去的颜色类名 |
|||
getHoverClass() { |
|||
// 如果开启水波纹效果,则不启用hover-class效果 |
|||
if (this.loading || this.disabled || this.ripple || this.hoverClass) return ''; |
|||
let hoverClass = ''; |
|||
hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover'; |
|||
return hoverClass; |
|||
}, |
|||
// 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象 |
|||
showHairLineBorder() { |
|||
if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) { |
|||
return ''; |
|||
} else { |
|||
return 'u-hairline-border'; |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
let btnTimerId = this.timerId || "button_" + Math.floor(Math.random() * 100000000 + 0); |
|||
return { |
|||
btnTimerId, |
|||
rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离 |
|||
rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离 |
|||
fields: {}, // 波纹按钮节点信息 |
|||
waveActive: false // 激活水波纹 |
|||
}; |
|||
}, |
|||
methods: { |
|||
// 按钮点击 |
|||
click(e) { |
|||
// 进行节流控制,每this.throttle毫秒内,只在开始处执行 |
|||
this.$u.throttle(() => { |
|||
// 如果按钮时disabled和loading状态,不触发水波纹效果 |
|||
if (this.loading === true || this.disabled === true) return; |
|||
// 是否开启水波纹效果 |
|||
if (this.ripple) { |
|||
// 每次点击时,移除上一次的类,再次添加,才能触发动画效果 |
|||
this.waveActive = false; |
|||
this.$nextTick(function() { |
|||
this.getWaveQuery(e); |
|||
}); |
|||
} |
|||
this.$emit('click', e); |
|||
}, this.throttleTime, true, this.btnTimerId); |
|||
}, |
|||
// 查询按钮的节点信息 |
|||
getWaveQuery(e) { |
|||
this.getElQuery().then(res => { |
|||
// 查询返回的是一个数组节点 |
|||
let data = res[0]; |
|||
// 查询不到节点信息,不操作 |
|||
if (!data.width || !data.width) return; |
|||
// 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边 |
|||
// 最终的方形(变换后的圆形)才能覆盖整个按钮 |
|||
data.targetWidth = data.height > data.width ? data.height : data.width; |
|||
if (!data.targetWidth) return; |
|||
this.fields = data; |
|||
let touchesX = '', |
|||
touchesY = ''; |
|||
// #ifdef MP-BAIDU |
|||
touchesX = e.changedTouches[0].clientX; |
|||
touchesY = e.changedTouches[0].clientY; |
|||
// #endif |
|||
// #ifdef MP-ALIPAY |
|||
touchesX = e.detail.clientX; |
|||
touchesY = e.detail.clientY; |
|||
// #endif |
|||
// #ifndef MP-BAIDU || MP-ALIPAY |
|||
touchesX = e.touches[0].clientX; |
|||
touchesY = e.touches[0].clientY; |
|||
// #endif |
|||
// 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top |
|||
// 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置 |
|||
// 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置 |
|||
this.rippleTop = touchesY - data.top - data.targetWidth / 2; |
|||
this.rippleLeft = touchesX - data.left - data.targetWidth / 2; |
|||
this.$nextTick(() => { |
|||
this.waveActive = true; |
|||
}); |
|||
}); |
|||
}, |
|||
// 获取节点信息 |
|||
getElQuery() { |
|||
return new Promise(resolve => { |
|||
let queryInfo = ''; |
|||
// 获取元素节点信息,请查看uniapp相关文档 |
|||
// https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect |
|||
queryInfo = uni.createSelectorQuery().in(this); |
|||
//#ifdef MP-ALIPAY |
|||
queryInfo = uni.createSelectorQuery(); |
|||
//#endif |
|||
queryInfo.select('.u-btn').boundingClientRect(); |
|||
queryInfo.exec(data => { |
|||
resolve(data); |
|||
}); |
|||
}); |
|||
}, |
|||
// 下面为对接uniapp官方按钮开放能力事件回调的对接 |
|||
getphonenumber(res) { |
|||
this.$emit('getphonenumber', res); |
|||
}, |
|||
getuserinfo(res) { |
|||
this.$emit('getuserinfo', res); |
|||
}, |
|||
error(res) { |
|||
this.$emit('error', res); |
|||
}, |
|||
opensetting(res) { |
|||
this.$emit('opensetting', res); |
|||
}, |
|||
launchapp(res) { |
|||
this.$emit('launchapp', res); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import '../../libs/css/style.components.scss'; |
|||
.u-btn::after { |
|||
border: none; |
|||
} |
|||
|
|||
.u-btn { |
|||
position: relative; |
|||
border: 0; |
|||
//border-radius: 10rpx; |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
// 避免边框某些场景可能被“裁剪”,不能设置为hidden |
|||
overflow: visible; |
|||
line-height: 1; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
cursor: pointer; |
|||
padding: 0 40rpx; |
|||
z-index: 1; |
|||
box-sizing: border-box; |
|||
transition: all 0.15s; |
|||
|
|||
&--bold-border { |
|||
border: 1px solid #ffffff; |
|||
} |
|||
|
|||
&--default { |
|||
color: $u-content-color; |
|||
border-color: #c0c4cc; |
|||
background-color: #ffffff; |
|||
} |
|||
|
|||
&--primary { |
|||
color: #ffffff; |
|||
border-color: $u-type-primary; |
|||
background-color: $u-type-primary; |
|||
} |
|||
|
|||
&--success { |
|||
color: #ffffff; |
|||
border-color: $u-type-success; |
|||
background-color: $u-type-success; |
|||
} |
|||
|
|||
&--error { |
|||
color: #ffffff; |
|||
border-color: $u-type-error; |
|||
background-color: $u-type-error; |
|||
} |
|||
|
|||
&--warning { |
|||
color: #ffffff; |
|||
border-color: $u-type-warning; |
|||
background-color: $u-type-warning; |
|||
} |
|||
|
|||
&--default--disabled { |
|||
color: #ffffff; |
|||
border-color: #e4e7ed; |
|||
background-color: #ffffff; |
|||
} |
|||
|
|||
&--primary--disabled { |
|||
color: #ffffff!important; |
|||
border-color: $u-type-primary-disabled!important; |
|||
background-color: $u-type-primary-disabled!important; |
|||
} |
|||
|
|||
&--success--disabled { |
|||
color: #ffffff!important; |
|||
border-color: $u-type-success-disabled!important; |
|||
background-color: $u-type-success-disabled!important; |
|||
} |
|||
|
|||
&--error--disabled { |
|||
color: #ffffff!important; |
|||
border-color: $u-type-error-disabled!important; |
|||
background-color: $u-type-error-disabled!important; |
|||
} |
|||
|
|||
&--warning--disabled { |
|||
color: #ffffff!important; |
|||
border-color: $u-type-warning-disabled!important; |
|||
background-color: $u-type-warning-disabled!important; |
|||
} |
|||
|
|||
&--primary--plain { |
|||
color: $u-type-primary!important; |
|||
border-color: $u-type-primary-disabled!important; |
|||
background-color: $u-type-primary-light!important; |
|||
} |
|||
|
|||
&--success--plain { |
|||
color: $u-type-success!important; |
|||
border-color: $u-type-success-disabled!important; |
|||
background-color: $u-type-success-light!important; |
|||
} |
|||
|
|||
&--error--plain { |
|||
color: $u-type-error!important; |
|||
border-color: $u-type-error-disabled!important; |
|||
background-color: $u-type-error-light!important; |
|||
} |
|||
|
|||
&--warning--plain { |
|||
color: $u-type-warning!important; |
|||
border-color: $u-type-warning-disabled!important; |
|||
background-color: $u-type-warning-light!important; |
|||
} |
|||
} |
|||
|
|||
.u-hairline-border:after { |
|||
content: ' '; |
|||
position: absolute; |
|||
pointer-events: none; |
|||
// 设置为border-box,意味着下面的scale缩小为0.5,实际上缩小的是伪元素的内容(border-box意味着内容不含border) |
|||
box-sizing: border-box; |
|||
// 中心点作为变形(scale())的原点 |
|||
-webkit-transform-origin: 0 0; |
|||
transform-origin: 0 0; |
|||
left: 0; |
|||
top: 0; |
|||
width: 199.8%; |
|||
height: 199.7%; |
|||
-webkit-transform: scale(0.5, 0.5); |
|||
transform: scale(0.5, 0.5); |
|||
border: 1px solid currentColor; |
|||
z-index: 1; |
|||
} |
|||
|
|||
.u-wave-ripple { |
|||
z-index: 0; |
|||
position: absolute; |
|||
border-radius: 100%; |
|||
background-clip: padding-box; |
|||
pointer-events: none; |
|||
user-select: none; |
|||
transform: scale(0); |
|||
opacity: 1; |
|||
transform-origin: center; |
|||
} |
|||
|
|||
.u-wave-ripple.u-wave-active { |
|||
opacity: 0; |
|||
transform: scale(2); |
|||
transition: opacity 1s linear, transform 0.4s linear; |
|||
} |
|||
|
|||
.u-round-circle { |
|||
border-radius: 100rpx; |
|||
} |
|||
|
|||
.u-round-circle::after { |
|||
border-radius: 100rpx; |
|||
} |
|||
|
|||
.u-loading::after { |
|||
background-color: hsla(0, 0%, 100%, 0.35); |
|||
} |
|||
|
|||
.u-size-default { |
|||
font-size: 30rpx; |
|||
height: 80rpx; |
|||
line-height: 80rpx; |
|||
} |
|||
|
|||
.u-size-medium { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
width: auto; |
|||
font-size: 26rpx; |
|||
height: 70rpx; |
|||
line-height: 70rpx; |
|||
padding: 0 80rpx; |
|||
} |
|||
|
|||
.u-size-mini { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
width: auto; |
|||
font-size: 22rpx; |
|||
padding-top: 1px; |
|||
height: 50rpx; |
|||
line-height: 50rpx; |
|||
padding: 0 20rpx; |
|||
} |
|||
|
|||
.u-primary-plain-hover { |
|||
color: #ffffff !important; |
|||
background: $u-type-primary-dark !important; |
|||
} |
|||
|
|||
.u-default-plain-hover { |
|||
color: $u-type-primary-dark !important; |
|||
background: $u-type-primary-light !important; |
|||
} |
|||
|
|||
.u-success-plain-hover { |
|||
color: #ffffff !important; |
|||
background: $u-type-success-dark !important; |
|||
} |
|||
|
|||
.u-warning-plain-hover { |
|||
color: #ffffff !important; |
|||
background: $u-type-warning-dark !important; |
|||
} |
|||
|
|||
.u-error-plain-hover { |
|||
color: #ffffff !important; |
|||
background: $u-type-error-dark !important; |
|||
} |
|||
|
|||
.u-info-plain-hover { |
|||
color: #ffffff !important; |
|||
background: $u-type-info-dark !important; |
|||
} |
|||
|
|||
.u-default-hover { |
|||
color: $u-type-primary-dark !important; |
|||
border-color: $u-type-primary-dark !important; |
|||
background-color: $u-type-primary-light !important; |
|||
} |
|||
|
|||
.u-primary-hover { |
|||
background: $u-type-primary-dark !important; |
|||
color: #fff; |
|||
} |
|||
|
|||
.u-success-hover { |
|||
background: $u-type-success-dark !important; |
|||
color: #fff; |
|||
} |
|||
|
|||
.u-info-hover { |
|||
background: $u-type-info-dark !important; |
|||
color: #fff; |
|||
} |
|||
|
|||
.u-warning-hover { |
|||
background: $u-type-warning-dark !important; |
|||
color: #fff; |
|||
} |
|||
|
|||
.u-error-hover { |
|||
background: $u-type-error-dark !important; |
|||
color: #fff; |
|||
} |
|||
</style> |
@ -0,0 +1,661 @@ |
|||
<template> |
|||
<u-popup closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="popupValue" length="auto" |
|||
:safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable"> |
|||
<view class="u-calendar"> |
|||
<view class="u-calendar__header"> |
|||
<view class="u-calendar__header__text" v-if="!$slots['tooltip']"> |
|||
{{toolTip}} |
|||
</view> |
|||
<slot v-else name="tooltip" /> |
|||
</view> |
|||
<view class="u-calendar__action u-flex u-row-center"> |
|||
<view class="u-calendar__action__icon"> |
|||
<u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon> |
|||
</view> |
|||
<view class="u-calendar__action__icon"> |
|||
<u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon> |
|||
</view> |
|||
<view class="u-calendar__action__text">{{ showTitle }}</view> |
|||
<view class="u-calendar__action__icon"> |
|||
<u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon> |
|||
</view> |
|||
<view class="u-calendar__action__icon"> |
|||
<u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon> |
|||
</view> |
|||
</view> |
|||
<view class="u-calendar__week-day"> |
|||
<view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view> |
|||
</view> |
|||
<view class="u-calendar__content"> |
|||
<!-- 前置空白部分 --> |
|||
<block v-for="(item, index) in weekdayArr" :key="index"> |
|||
<view class="u-calendar__content__item"></view> |
|||
</block> |
|||
<view class="u-calendar__content__item" :class="{ |
|||
'u-hover-class':openDisAbled(year,month,index+1), |
|||
'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date', |
|||
'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date' |
|||
}" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index" |
|||
@tap="dateClick(index)"> |
|||
<view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}"> |
|||
<view>{{ index + 1 }}</view> |
|||
</view> |
|||
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view> |
|||
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view> |
|||
</view> |
|||
<view class="u-calendar__content__bg-month">{{month}}</view> |
|||
</view> |
|||
<view class="u-calendar__bottom"> |
|||
<view class="u-calendar__bottom__choose"> |
|||
<text>{{mode == 'date' ? activeDate : startDate}}</text> |
|||
<text v-if="endDate">至{{endDate}}</text> |
|||
</view> |
|||
<view class="u-calendar__bottom__btn"> |
|||
<u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</u-popup> |
|||
</template> |
|||
<script> |
|||
/** |
|||
* calendar 日历 |
|||
* @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。 |
|||
* @tutorial http://uviewui.com/components/calendar.html |
|||
* @property {String} mode 选择日期的模式,date-为单个日期,range-为选择日期范围 |
|||
* @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起 |
|||
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false) |
|||
* @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true) |
|||
* @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true) |
|||
* @property {String Number} max-year 可切换的最大年份(默认2050) |
|||
* @property {String Number} min-year 最小可选日期(默认1950) |
|||
* @property {String Number} min-date 可切换的最小年份(默认1950-01-01) |
|||
* @property {String Number} max-date 最大可选日期(默认当前日期) |
|||
* @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20) |
|||
* @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true) |
|||
* @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266) |
|||
* @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399) |
|||
* @property {String} color 日期字体的默认颜色(默认#303133) |
|||
* @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff) |
|||
* @property {String Number} z-index 弹出时的z-index值(默认10075) |
|||
* @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff) |
|||
* @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13)) |
|||
* @property {String} range-color 选择范围内字体颜色(默认#2979ff) |
|||
* @property {String} start-text 起始日期底部的提示文字(默认 '开始') |
|||
* @property {String} end-text 结束日期底部的提示文字(默认 '结束') |
|||
* @property {String} btn-type 底部确定按钮的主题(默认 'primary') |
|||
* @property {String} toolTip 顶部提示文字,如设置名为tooltip的slot,此参数将失效(默认 '选择日期') |
|||
* @property {Boolean} closeable 是否显示右上角的关闭图标(默认true) |
|||
* @example <u-calendar v-model="show" :mode="mode"></u-calendar> |
|||
*/ |
|||
|
|||
export default { |
|||
name: 'u-calendar', |
|||
emits: ["update:modelValue", "input", "change"], |
|||
props: { |
|||
// 通过双向绑定控制组件的弹出与收起 |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
modelValue: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
safeAreaInsetBottom: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否允许通过点击遮罩关闭Picker |
|||
maskCloseAble: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 弹出的z-index值 |
|||
zIndex: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 是否允许切换年份 |
|||
changeYear: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否允许切换月份 |
|||
changeMonth: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// date-单个日期选择,range-开始日期+结束日期选择 |
|||
mode: { |
|||
type: String, |
|||
default: 'date' |
|||
}, |
|||
// 可切换的最大年份 |
|||
maxYear: { |
|||
type: [Number, String], |
|||
default: 2050 |
|||
}, |
|||
// 可切换的最小年份 |
|||
minYear: { |
|||
type: [Number, String], |
|||
default: 1950 |
|||
}, |
|||
// 最小可选日期(不在范围内日期禁用不可选) |
|||
minDate: { |
|||
type: [Number, String], |
|||
default: '1950-01-01' |
|||
}, |
|||
/** |
|||
* 最大可选日期 |
|||
* 默认最大值为今天,之后的日期不可选 |
|||
* 2030-12-31 |
|||
* */ |
|||
maxDate: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 弹窗顶部左右两边的圆角值 |
|||
borderRadius: { |
|||
type: [String, Number], |
|||
default: 20 |
|||
}, |
|||
// 月份切换按钮箭头颜色 |
|||
monthArrowColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// 年份切换按钮箭头颜色 |
|||
yearArrowColor: { |
|||
type: String, |
|||
default: '#909399' |
|||
}, |
|||
// 默认日期字体颜色 |
|||
color: { |
|||
type: String, |
|||
default: '#303133' |
|||
}, |
|||
// 选中|起始结束日期背景色 |
|||
activeBgColor: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
}, |
|||
// 选中|起始结束日期字体颜色 |
|||
activeColor: { |
|||
type: String, |
|||
default: '#ffffff' |
|||
}, |
|||
// 范围内日期背景色 |
|||
rangeBgColor: { |
|||
type: String, |
|||
default: 'rgba(41,121,255,0.13)' |
|||
}, |
|||
// 范围内日期字体颜色 |
|||
rangeColor: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
}, |
|||
// mode=range时生效,起始日期自定义文案 |
|||
startText: { |
|||
type: String, |
|||
default: '开始' |
|||
}, |
|||
// mode=range时生效,结束日期自定义文案 |
|||
endText: { |
|||
type: String, |
|||
default: '结束' |
|||
}, |
|||
//按钮样式类型 |
|||
btnType: { |
|||
type: String, |
|||
default: 'primary' |
|||
}, |
|||
// 当前选中日期带选中效果 |
|||
isActiveCurrent: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 切换年月是否触发事件 mode=date时生效 |
|||
isChange: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否显示右上角的关闭图标 |
|||
closeable: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 顶部的提示文字 |
|||
toolTip: { |
|||
type: String, |
|||
default: '选择日期' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
popupValue:false, |
|||
// 星期几,值为1-7 |
|||
weekday: 1, |
|||
weekdayArr:[], |
|||
// 当前月有多少天 |
|||
days: 0, |
|||
daysArr:[], |
|||
showTitle: '', |
|||
year: 2020, |
|||
month: 0, |
|||
day: 0, |
|||
startYear: 0, |
|||
startMonth: 0, |
|||
startDay: 0, |
|||
endYear: 0, |
|||
endMonth: 0, |
|||
endDay: 0, |
|||
today: '', |
|||
activeDate: '', |
|||
startDate: '', |
|||
endDate: '', |
|||
isStart: true, |
|||
min: null, |
|||
max: null, |
|||
weekDayZh: ['日', '一', '二', '三', '四', '五', '六'] |
|||
}; |
|||
}, |
|||
computed: { |
|||
dataChange() { |
|||
return `${this.mode}-${this.minDate}-${this.maxDate}`; |
|||
}, |
|||
uZIndex() { |
|||
// 如果用户有传递z-index值,优先使用 |
|||
return this.zIndex ? this.zIndex : this.$u.zIndex.popup; |
|||
} |
|||
}, |
|||
watch: { |
|||
dataChange(val) { |
|||
this.init() |
|||
}, |
|||
value(v1, v2) { |
|||
this.popupValue = v1; |
|||
}, |
|||
modelValue(v1, v2) { |
|||
this.popupValue = v1; |
|||
}, |
|||
}, |
|||
created() { |
|||
this.init() |
|||
}, |
|||
methods: { |
|||
getValue(){ |
|||
// #ifndef VUE3 |
|||
return this.value; |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
return this.modelValue; |
|||
// #endif |
|||
}, |
|||
getColor(index, type) { |
|||
let color = type == 1 ? '' : this.color; |
|||
let day = index + 1 |
|||
let date = `${this.year}-${this.month}-${day}` |
|||
let timestamp = new Date(date.replace(/\-/g, '/')).getTime(); |
|||
let start = this.startDate.replace(/\-/g, '/') |
|||
let end = this.endDate.replace(/\-/g, '/') |
|||
if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) { |
|||
color = type == 1 ? this.activeBgColor : this.activeColor; |
|||
} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) { |
|||
color = type == 1 ? this.rangeBgColor : this.rangeColor; |
|||
} |
|||
return color; |
|||
}, |
|||
init() { |
|||
let now = new Date(); |
|||
this.year = now.getFullYear(); |
|||
this.month = now.getMonth() + 1; |
|||
this.day = now.getDate(); |
|||
this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`; |
|||
this.activeDate = this.today; |
|||
this.min = this.initDate(this.minDate); |
|||
this.max = this.initDate(this.maxDate || this.today); |
|||
this.startDate = ""; |
|||
this.startYear = 0; |
|||
this.startMonth = 0; |
|||
this.startDay = 0; |
|||
this.endYear = 0; |
|||
this.endMonth = 0; |
|||
this.endDay = 0; |
|||
this.endDate = ""; |
|||
this.isStart = true; |
|||
this.changeData(); |
|||
}, |
|||
//日期处理 |
|||
initDate(date) { |
|||
let fdate = date.split('-'); |
|||
return { |
|||
year: Number(fdate[0] || 1920), |
|||
month: Number(fdate[1] || 1), |
|||
day: Number(fdate[2] || 1) |
|||
} |
|||
}, |
|||
openDisAbled: function(year, month, day) { |
|||
let bool = true; |
|||
let date = `${year}/${month}/${day}`; |
|||
// let today = this.today.replace(/\-/g, '/'); |
|||
let min = `${this.min.year}/${this.min.month}/${this.min.day}`; |
|||
let max = `${this.max.year}/${this.max.month}/${this.max.day}`; |
|||
let timestamp = new Date(date).getTime(); |
|||
if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) { |
|||
bool = false; |
|||
} |
|||
return bool; |
|||
}, |
|||
generateArray: function(start, end) { |
|||
return Array.from(new Array(end + 1).keys()).slice(start); |
|||
}, |
|||
formatNum: function(num) { |
|||
return num < 10 ? '0' + num : num + ''; |
|||
}, |
|||
//一个月有多少天 |
|||
getMonthDay(year, month) { |
|||
let days = new Date(year, month, 0).getDate(); |
|||
return days; |
|||
}, |
|||
getWeekday(year, month) { |
|||
let date = new Date(`${year}/${month}/01 00:00:00`); |
|||
return date.getDay(); |
|||
}, |
|||
checkRange(year) { |
|||
let overstep = false; |
|||
if (year < this.minYear || year > this.maxYear) { |
|||
uni.showToast({ |
|||
title: "日期超出范围啦~", |
|||
icon: 'none' |
|||
}) |
|||
overstep = true; |
|||
} |
|||
return overstep; |
|||
}, |
|||
changeMonthHandler(isAdd) { |
|||
if (isAdd) { |
|||
let month = this.month + 1; |
|||
let year = month > 12 ? this.year + 1 : this.year; |
|||
if (!this.checkRange(year)) { |
|||
this.month = month > 12 ? 1 : month; |
|||
this.year = year; |
|||
this.changeData(); |
|||
} |
|||
|
|||
} else { |
|||
let month = this.month - 1; |
|||
let year = month < 1 ? this.year - 1 : this.year; |
|||
if (!this.checkRange(year)) { |
|||
this.month = month < 1 ? 12 : month; |
|||
this.year = year; |
|||
this.changeData(); |
|||
} |
|||
} |
|||
}, |
|||
changeYearHandler(isAdd) { |
|||
let year = isAdd ? this.year + 1 : this.year - 1; |
|||
if (!this.checkRange(year)) { |
|||
this.year = year; |
|||
this.changeData(); |
|||
} |
|||
}, |
|||
changeData() { |
|||
this.days = this.getMonthDay(this.year, this.month); |
|||
this.daysArr=this.generateArray(1,this.days) |
|||
this.weekday = this.getWeekday(this.year, this.month); |
|||
this.weekdayArr=this.generateArray(1,this.weekday) |
|||
this.showTitle = `${this.year}年${this.month}月`; |
|||
if (this.isChange && this.mode == 'date') { |
|||
this.btnFix(true); |
|||
} |
|||
}, |
|||
dateClick: function(day) { |
|||
day += 1; |
|||
if (!this.openDisAbled(this.year, this.month, day)) { |
|||
this.day = day; |
|||
let date = `${this.year}-${this.month}-${day}`; |
|||
if (this.mode == 'date') { |
|||
this.activeDate = date; |
|||
} else { |
|||
let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime() |
|||
if (this.isStart || compare) { |
|||
this.startDate = date; |
|||
this.startYear = this.year; |
|||
this.startMonth = this.month; |
|||
this.startDay = this.day; |
|||
this.endYear = 0; |
|||
this.endMonth = 0; |
|||
this.endDay = 0; |
|||
this.endDate = ""; |
|||
this.activeDate = ""; |
|||
this.isStart = false; |
|||
} else { |
|||
this.endDate = date; |
|||
this.endYear = this.year; |
|||
this.endMonth = this.month; |
|||
this.endDay = this.day; |
|||
this.isStart = true; |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
close() { |
|||
// 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗 |
|||
this.$emit('input', false); |
|||
this.$emit("update:modelValue", false); |
|||
}, |
|||
getWeekText(date) { |
|||
date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`); |
|||
let week = date.getDay(); |
|||
return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week]; |
|||
}, |
|||
btnFix(show) { |
|||
if (!show) { |
|||
this.close(); |
|||
} |
|||
if (this.mode == 'date') { |
|||
let arr = this.activeDate.split('-') |
|||
let year = this.isChange ? this.year : Number(arr[0]); |
|||
let month = this.isChange ? this.month : Number(arr[1]); |
|||
let day = this.isChange ? this.day : Number(arr[2]); |
|||
//当前月有多少天 |
|||
let days = this.getMonthDay(year, month); |
|||
let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`; |
|||
let weekText = this.getWeekText(result); |
|||
let isToday = false; |
|||
if (`${year}-${month}-${day}` == this.today) { |
|||
//今天 |
|||
isToday = true; |
|||
} |
|||
this.$emit('change', { |
|||
year: year, |
|||
month: month, |
|||
day: day, |
|||
days: days, |
|||
result: result, |
|||
week: weekText, |
|||
isToday: isToday, |
|||
// switch: show //是否是切换年月操作 |
|||
}); |
|||
} else { |
|||
if (!this.startDate || !this.endDate) return; |
|||
let startMonth = this.formatNum(this.startMonth); |
|||
let startDay = this.formatNum(this.startDay); |
|||
let startDate = `${this.startYear}-${startMonth}-${startDay}`; |
|||
let startWeek = this.getWeekText(startDate) |
|||
|
|||
let endMonth = this.formatNum(this.endMonth); |
|||
let endDay = this.formatNum(this.endDay); |
|||
let endDate = `${this.endYear}-${endMonth}-${endDay}`; |
|||
let endWeek = this.getWeekText(endDate); |
|||
this.$emit('change', { |
|||
startYear: this.startYear, |
|||
startMonth: this.startMonth, |
|||
startDay: this.startDay, |
|||
startDate: startDate, |
|||
startWeek: startWeek, |
|||
endYear: this.endYear, |
|||
endMonth: this.endMonth, |
|||
endDay: this.endDay, |
|||
endDate: endDate, |
|||
endWeek: endWeek |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-calendar { |
|||
color: $u-content-color; |
|||
|
|||
&__header { |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
font-size: 30rpx; |
|||
background-color: #fff; |
|||
color: $u-main-color; |
|||
|
|||
&__text { |
|||
margin-top: 30rpx; |
|||
padding: 0 60rpx; |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
} |
|||
|
|||
&__action { |
|||
padding: 40rpx 0 40rpx 0; |
|||
|
|||
&__icon { |
|||
margin: 0 16rpx; |
|||
} |
|||
|
|||
&__text { |
|||
padding: 0 16rpx; |
|||
color: $u-main-color; |
|||
font-size: 32rpx; |
|||
line-height: 32rpx; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
|
|||
&__week-day { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 6px 0; |
|||
overflow: hidden; |
|||
|
|||
&__text { |
|||
flex: 1; |
|||
text-align: center; |
|||
} |
|||
} |
|||
|
|||
&__content { |
|||
width: 100%; |
|||
@include vue-flex; |
|||
flex-wrap: wrap; |
|||
padding: 6px 0; |
|||
box-sizing: border-box; |
|||
background-color: #fff; |
|||
position: relative; |
|||
|
|||
&--end-date { |
|||
border-top-right-radius: 8rpx; |
|||
border-bottom-right-radius: 8rpx; |
|||
} |
|||
|
|||
&--start-date { |
|||
border-top-left-radius: 8rpx; |
|||
border-bottom-left-radius: 8rpx; |
|||
} |
|||
|
|||
&__item { |
|||
width: 14.2857%; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 6px 0; |
|||
overflow: hidden; |
|||
position: relative; |
|||
z-index: 2; |
|||
|
|||
&__inner { |
|||
height: 84rpx; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-direction: column; |
|||
font-size: 32rpx; |
|||
position: relative; |
|||
border-radius: 50%; |
|||
|
|||
&__desc { |
|||
width: 100%; |
|||
font-size: 24rpx; |
|||
line-height: 24rpx; |
|||
transform: scale(0.75); |
|||
transform-origin: center center; |
|||
position: absolute; |
|||
left: 0; |
|||
text-align: center; |
|||
bottom: 2rpx; |
|||
} |
|||
} |
|||
|
|||
&__tips { |
|||
width: 100%; |
|||
font-size: 24rpx; |
|||
line-height: 24rpx; |
|||
position: absolute; |
|||
left: 0; |
|||
transform: scale(0.8); |
|||
transform-origin: center center; |
|||
text-align: center; |
|||
bottom: 8rpx; |
|||
z-index: 2; |
|||
} |
|||
} |
|||
|
|||
&__bg-month { |
|||
position: absolute; |
|||
font-size: 130px; |
|||
line-height: 130px; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
color: #e4e7ed; |
|||
z-index: 1; |
|||
} |
|||
} |
|||
|
|||
&__bottom { |
|||
width: 100%; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-direction: column; |
|||
background-color: #fff; |
|||
padding: 0 40rpx 30rpx; |
|||
box-sizing: border-box; |
|||
font-size: 24rpx; |
|||
color: $u-tips-color; |
|||
|
|||
&__choose { |
|||
height: 50rpx; |
|||
} |
|||
|
|||
&__btn { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,261 @@ |
|||
<template> |
|||
<view class="u-keyboard" @touchmove.stop.prevent="() => {}"> |
|||
<view class="u-keyboard-grids"> |
|||
<view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i"> |
|||
<view :hover-stay-time="100" @touchstart="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn" |
|||
v-for="(item, j) in group" :key="j"> |
|||
{{ item }} |
|||
</view> |
|||
</view> |
|||
<view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back" |
|||
hover-class="u-hover-class"> |
|||
<u-icon :size="38" name="backspace" :bold="true"></u-icon> |
|||
</view> |
|||
<view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @click="changeCarInputMode"> |
|||
<text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text> |
|||
/ |
|||
<text class="en" :class="[abc ? 'active' : 'inactive']">英</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "u-keyboard", |
|||
emits: ["change", "backspace"], |
|||
props: { |
|||
// 是否打乱键盘按键的顺序 |
|||
random: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
// 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称 |
|||
abc: false |
|||
}; |
|||
}, |
|||
computed: { |
|||
areaList() { |
|||
let data = [ |
|||
'京', |
|||
'沪', |
|||
'粤', |
|||
'津', |
|||
'冀', |
|||
'豫', |
|||
'云', |
|||
'辽', |
|||
'黑', |
|||
'湘', |
|||
'皖', |
|||
'鲁', |
|||
'苏', |
|||
'浙', |
|||
'赣', |
|||
'鄂', |
|||
'桂', |
|||
'甘', |
|||
'晋', |
|||
'陕', |
|||
'蒙', |
|||
'吉', |
|||
'闽', |
|||
'贵', |
|||
'渝', |
|||
'川', |
|||
'青', |
|||
'琼', |
|||
'宁', |
|||
'挂', |
|||
'藏', |
|||
'港', |
|||
'澳', |
|||
'新', |
|||
'使', |
|||
'学' |
|||
]; |
|||
let tmp = []; |
|||
// 打乱顺序 |
|||
if (this.random) data = this.$u.randomArray(data); |
|||
// 切割成二维数组 |
|||
tmp[0] = data.slice(0, 10); |
|||
tmp[1] = data.slice(10, 20); |
|||
tmp[2] = data.slice(20, 30); |
|||
tmp[3] = data.slice(30, 36); |
|||
return tmp; |
|||
}, |
|||
EngKeyBoardList() { |
|||
let data = [ |
|||
1, |
|||
2, |
|||
3, |
|||
4, |
|||
5, |
|||
6, |
|||
7, |
|||
8, |
|||
9, |
|||
0, |
|||
'Q', |
|||
'W', |
|||
'E', |
|||
'R', |
|||
'T', |
|||
'Y', |
|||
'U', |
|||
'I', |
|||
'O', |
|||
'P', |
|||
'A', |
|||
'S', |
|||
'D', |
|||
'F', |
|||
'G', |
|||
'H', |
|||
'J', |
|||
'K', |
|||
'L', |
|||
'Z', |
|||
'X', |
|||
'C', |
|||
'V', |
|||
'B', |
|||
'N', |
|||
'M' |
|||
]; |
|||
let tmp = []; |
|||
if (this.random) data = this.$u.randomArray(data); |
|||
tmp[0] = data.slice(0, 10); |
|||
tmp[1] = data.slice(10, 20); |
|||
tmp[2] = data.slice(20, 30); |
|||
tmp[3] = data.slice(30, 36); |
|||
return tmp; |
|||
} |
|||
}, |
|||
methods: { |
|||
// 点击键盘按钮 |
|||
carInputClick(i, j) { |
|||
let value = ''; |
|||
// 不同模式,获取不同数组的值 |
|||
if (this.abc) value = this.EngKeyBoardList[i][j]; |
|||
else value = this.areaList[i][j]; |
|||
if(!this.abc) this.abc = true; |
|||
this.$emit('change', value); |
|||
if(this.vibrate) uni.vibrateShort(); |
|||
}, |
|||
// 修改汽车牌键盘的输入模式,中文|英文 |
|||
changeCarInputMode() { |
|||
this.abc = !this.abc; |
|||
}, |
|||
// 点击退格键 |
|||
backspaceClick() { |
|||
this.backspaceFn(); |
|||
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器 |
|||
this.timer = null; |
|||
this.timer = setInterval(() => { |
|||
this.backspaceFn(); |
|||
}, 250); |
|||
}, |
|||
backspaceFn(){ |
|||
this.$emit('backspace'); |
|||
}, |
|||
clearTimer() { |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
}, |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-keyboard-grids { |
|||
background: rgb(215, 215, 217); |
|||
padding: 24rpx 0; |
|||
position: relative; |
|||
} |
|||
|
|||
.u-keyboard-grids-item { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.u-keyboard-grids-btn { |
|||
text-decoration: none; |
|||
width: 62rpx; |
|||
flex: 0 0 64rpx; |
|||
height: 80rpx; |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
font-size: 36rpx; |
|||
text-align: center; |
|||
line-height: 80rpx; |
|||
background-color: #fff; |
|||
margin: 8rpx 5rpx; |
|||
border-radius: 8rpx; |
|||
box-shadow: 0 2rpx 0rpx #888992; |
|||
font-weight: 500; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.u-carinput-hover { |
|||
background-color: rgb(185, 188, 195) !important; |
|||
} |
|||
|
|||
.u-keyboard-back { |
|||
position: absolute; |
|||
width: 96rpx; |
|||
right: 22rpx; |
|||
bottom: 32rpx; |
|||
height: 80rpx; |
|||
background-color: rgb(185, 188, 195); |
|||
@include vue-flex; |
|||
align-items: center; |
|||
border-radius: 8rpx; |
|||
justify-content: center; |
|||
box-shadow: 0 2rpx 0rpx #888992; |
|||
} |
|||
|
|||
.u-keyboard-change { |
|||
font-size: 24rpx; |
|||
box-shadow: 0 2rpx 0rpx #888992; |
|||
position: absolute; |
|||
width: 96rpx; |
|||
left: 22rpx; |
|||
line-height: 1; |
|||
bottom: 32rpx; |
|||
height: 80rpx; |
|||
background-color: #ffffff; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
border-radius: 8rpx; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.u-keyboard-change .inactive.zh { |
|||
transform: scale(0.85) translateY(-10rpx); |
|||
} |
|||
|
|||
.u-keyboard-change .inactive.en { |
|||
transform: scale(0.85) translateY(10rpx); |
|||
} |
|||
|
|||
.u-keyboard-change .active { |
|||
color: rgb(237, 112, 64); |
|||
font-size: 30rpx; |
|||
} |
|||
|
|||
.u-keyboard-change .zh { |
|||
transform: translateY(-10rpx); |
|||
} |
|||
|
|||
.u-keyboard-change .en { |
|||
transform: translateY(10rpx); |
|||
} |
|||
</style> |
@ -0,0 +1,300 @@ |
|||
<template> |
|||
<view |
|||
class="u-card" |
|||
@tap.stop="click" |
|||
:class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }" |
|||
:style="{ |
|||
borderRadius: borderRadius + 'rpx', |
|||
margin: margin, |
|||
boxShadow: boxShadow |
|||
}" |
|||
> |
|||
<view |
|||
v-if="showHead" |
|||
class="u-card__head" |
|||
:style="[{padding: padding + 'rpx'}, headStyle]" |
|||
:class="{ |
|||
'u-border-bottom': headBorderBottom |
|||
}" |
|||
@tap="headClick" |
|||
> |
|||
<view v-if="!$slots.head" class="u-flex u-row-between"> |
|||
<view class="u-card__head--left u-flex u-line-1" v-if="title"> |
|||
<image |
|||
:src="thumb" |
|||
class="u-card__head--left__thumb" |
|||
mode="aspectfull" |
|||
v-if="thumb" |
|||
:style="{ |
|||
height: thumbWidth + 'rpx', |
|||
width: thumbWidth + 'rpx', |
|||
borderRadius: thumbCircle ? '100rpx' : '6rpx' |
|||
}" |
|||
></image> |
|||
<text |
|||
class="u-card__head--left__title u-line-1" |
|||
:style="{ |
|||
fontSize: titleSize + 'rpx', |
|||
color: titleColor |
|||
}" |
|||
> |
|||
{{ title }} |
|||
</text> |
|||
</view> |
|||
<view class="u-card__head--right u-line-1" v-if="subTitle"> |
|||
<text |
|||
class="u-card__head__title__text" |
|||
:style="{ |
|||
fontSize: subTitleSize + 'rpx', |
|||
color: subTitleColor |
|||
}" |
|||
> |
|||
{{ subTitle }} |
|||
</text> |
|||
</view> |
|||
</view> |
|||
<slot name="head" v-else /> |
|||
</view> |
|||
<view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view> |
|||
<view |
|||
v-if="showFoot" |
|||
class="u-card__foot" |
|||
@tap="footClick" |
|||
:style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]" |
|||
:class="{ |
|||
'u-border-top': footBorderTop |
|||
}" |
|||
> |
|||
<slot name="foot" /> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* card 卡片 |
|||
* @description 卡片组件一般用于多个列表条目,且风格统一的场景 |
|||
* @tutorial https://www.uviewui.com/components/card.html |
|||
* @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false) |
|||
* @property {String} title 头部左边的标题 |
|||
* @property {String} title-color 标题颜色(默认#303133) |
|||
* @property {String | Number} title-size 标题字体大小,单位rpx(默认30) |
|||
* @property {String} sub-title 头部右边的副标题 |
|||
* @property {String} sub-title-color 副标题颜色(默认#909399) |
|||
* @property {String | Number} sub-title-size 副标题字体大小(默认26) |
|||
* @property {Boolean} border 是否显示边框(默认true) |
|||
* @property {String | Number} index 用于标识点击了第几个卡片 |
|||
* @property {String} box-shadow 卡片外围阴影,字符串形式(默认none) |
|||
* @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"(默认30rpx) |
|||
* @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认16) |
|||
* @property {Object} head-style 头部自定义样式,对象形式 |
|||
* @property {Object} body-style 中部自定义样式,对象形式 |
|||
* @property {Object} foot-style 底部自定义样式,对象形式 |
|||
* @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true) |
|||
* @property {Boolean} foot-border-top 是否显示底部的上边框(默认true) |
|||
* @property {Boolean} show-head 是否显示头部(默认true) |
|||
* @property {Boolean} show-head 是否显示尾部(默认true) |
|||
* @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径 |
|||
* @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位rpx(默认60) |
|||
* @property {Boolean} thumb-circle 缩略图是否为圆形(默认false) |
|||
* @event {Function} click 整个卡片任意位置被点击时触发 |
|||
* @event {Function} head-click 卡片头部被点击时触发 |
|||
* @event {Function} body-click 卡片主体部分被点击时触发 |
|||
* @event {Function} foot-click 卡片底部部分被点击时触发 |
|||
* @example <u-card padding="30" title="card"></u-card> |
|||
*/ |
|||
export default { |
|||
name: 'u-card', |
|||
emits: ["click", "head-click", "body-click", "foot-click"], |
|||
props: { |
|||
// 与屏幕两侧是否留空隙 |
|||
full: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 标题 |
|||
title: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 标题颜色 |
|||
titleColor: { |
|||
type: String, |
|||
default: '#303133' |
|||
}, |
|||
// 标题字体大小,单位rpx |
|||
titleSize: { |
|||
type: [Number, String], |
|||
default: '30' |
|||
}, |
|||
// 副标题 |
|||
subTitle: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 副标题颜色 |
|||
subTitleColor: { |
|||
type: String, |
|||
default: '#909399' |
|||
}, |
|||
// 副标题字体大小,单位rpx |
|||
subTitleSize: { |
|||
type: [Number, String], |
|||
default: '26' |
|||
}, |
|||
// 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时) |
|||
border: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 用于标识点击了第几个 |
|||
index: { |
|||
type: [Number, String, Object], |
|||
default: '' |
|||
}, |
|||
// 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx" |
|||
margin: { |
|||
type: String, |
|||
default: '30rpx' |
|||
}, |
|||
// card卡片的圆角 |
|||
borderRadius: { |
|||
type: [Number, String], |
|||
default: '16' |
|||
}, |
|||
// 头部自定义样式,对象形式 |
|||
headStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 主体自定义样式,对象形式 |
|||
bodyStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 底部自定义样式,对象形式 |
|||
footStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 头部是否下边框 |
|||
headBorderBottom: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 底部是否有上边框 |
|||
footBorderTop: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 标题左边的缩略图 |
|||
thumb: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 缩略图宽高,单位rpx |
|||
thumbWidth: { |
|||
type: [String, Number], |
|||
default: '60' |
|||
}, |
|||
// 缩略图是否为圆形 |
|||
thumbCircle: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 给head,body,foot的内边距 |
|||
padding: { |
|||
type: [String, Number], |
|||
default: '30' |
|||
}, |
|||
// 是否显示头部 |
|||
showHead: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示尾部 |
|||
showFoot: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 卡片外围阴影,字符串形式 |
|||
boxShadow: { |
|||
type: String, |
|||
default: 'none' |
|||
} |
|||
}, |
|||
data() { |
|||
return {}; |
|||
}, |
|||
methods: { |
|||
click() { |
|||
this.$emit('click', this.index); |
|||
}, |
|||
headClick() { |
|||
this.$emit('head-click', this.index); |
|||
}, |
|||
bodyClick() { |
|||
this.$emit('body-click', this.index); |
|||
}, |
|||
footClick() { |
|||
this.$emit('foot-click', this.index); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-card { |
|||
position: relative; |
|||
overflow: hidden; |
|||
font-size: 28rpx; |
|||
background-color: #ffffff; |
|||
box-sizing: border-box; |
|||
|
|||
&-full { |
|||
// 如果是与屏幕之间不留空隙,应该设置左右边距为0 |
|||
margin-left: 0 !important; |
|||
margin-right: 0 !important; |
|||
width: 100%; |
|||
} |
|||
|
|||
&--border:after { |
|||
border-radius: 16rpx; |
|||
} |
|||
|
|||
&__head { |
|||
&--left { |
|||
color: $u-main-color; |
|||
|
|||
&__thumb { |
|||
margin-right: 16rpx; |
|||
} |
|||
|
|||
&__title { |
|||
max-width: 400rpx; |
|||
} |
|||
} |
|||
|
|||
&--right { |
|||
color: $u-tips-color; |
|||
margin-left: 6rpx; |
|||
} |
|||
} |
|||
|
|||
&__body { |
|||
color: $u-content-color; |
|||
} |
|||
|
|||
&__foot { |
|||
color: $u-tips-color; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,70 @@ |
|||
<template> |
|||
<view class="u-cell-box"> |
|||
<view class="u-cell-title" v-if="title" :style="[titleStyle]"> |
|||
{{title}} |
|||
</view> |
|||
<view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}"> |
|||
<slot /> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* cellGroup 单元格父组件Group |
|||
* @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item |
|||
* @tutorial https://www.uviewui.com/components/cell.html |
|||
* @property {String} title 分组标题 |
|||
* @property {Boolean} border 是否显示外边框(默认true) |
|||
* @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'} |
|||
* @example <u-cell-group title="设置喜好"> |
|||
*/ |
|||
export default { |
|||
name: "u-cell-group", |
|||
props: { |
|||
// 分组标题 |
|||
title: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否显示分组list上下边框 |
|||
border: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 分组标题的样式,对象形式,注意驼峰属性写法 |
|||
// 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'} |
|||
titleStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {}; |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
index: 0, |
|||
} |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-cell-box { |
|||
width: 100%; |
|||
} |
|||
|
|||
.u-cell-title { |
|||
padding: 30rpx 32rpx 10rpx 32rpx; |
|||
font-size: 30rpx; |
|||
text-align: left; |
|||
color: $u-tips-color; |
|||
} |
|||
|
|||
.u-cell-item-box { |
|||
background-color: #FFFFFF; |
|||
flex-direction: row; |
|||
} |
|||
</style> |
@ -0,0 +1,317 @@ |
|||
<template> |
|||
<view |
|||
@tap="click" |
|||
class="u-cell" |
|||
:class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }" |
|||
hover-stay-time="150" |
|||
:hover-class="hoverClass" |
|||
:style="{ |
|||
backgroundColor: bgColor |
|||
}" |
|||
> |
|||
<u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon> |
|||
<view class="u-flex" v-else> |
|||
<slot name="icon"></slot> |
|||
</view> |
|||
<view |
|||
class="u-cell_title" |
|||
:style="[ |
|||
{ |
|||
width: titleWidth ? titleWidth + 'rpx' : 'auto' |
|||
}, |
|||
titleStyle |
|||
]" |
|||
> |
|||
<block v-if="title !== ''">{{ title }}</block> |
|||
<slot name="title" v-else></slot> |
|||
|
|||
<view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]"> |
|||
<block v-if="label !== ''">{{ label }}</block> |
|||
<slot name="label" v-else></slot> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="u-cell__value" :style="[valueStyle]"> |
|||
<block class="u-cell__value" v-if="value !== ''">{{ value }}</block> |
|||
<slot v-else></slot> |
|||
</view> |
|||
<view class="u-flex u-cell_right" v-if="$slots['right-icon']"> |
|||
<slot name="right-icon"></slot> |
|||
</view> |
|||
<u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* cellItem 单元格Item |
|||
* @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用 |
|||
* @tutorial https://www.uviewui.com/components/cell.html |
|||
* @property {String} title 左侧标题 |
|||
* @property {String} icon 左侧图标名,只支持uView内置图标,见Icon 图标 |
|||
* @property {Object} icon-style 左边图标的样式,对象形式 |
|||
* @property {String} value 右侧内容 |
|||
* @property {String} label 标题下方的描述信息 |
|||
* @property {Boolean} border-bottom 是否显示cell的下边框(默认true) |
|||
* @property {Boolean} border-top 是否显示cell的上边框(默认false) |
|||
* @property {Boolean} center 是否使内容垂直居中(默认false) |
|||
* @property {String} hover-class 是否开启点击反馈,none为无效果(默认true) |
|||
* // @property {Boolean} border-gap border-bottom为true时,Cell列表中间的条目的下边框是否与左边有一个间隔(默认true) |
|||
* @property {Boolean} arrow 是否显示右侧箭头(默认true) |
|||
* @property {Boolean} required 箭头方向,可选值(默认right) |
|||
* @property {Boolean} arrow-direction 是否显示左边表示必填的星号(默认false) |
|||
* @property {Object} title-style 标题样式,对象形式 |
|||
* @property {Object} value-style 右侧内容样式,对象形式 |
|||
* @property {Object} label-style 标题下方描述信息的样式,对象形式 |
|||
* @property {String} bg-color 背景颜色(默认transparent) |
|||
* @property {String Number} index 用于在click事件回调中返回,标识当前是第几个Item |
|||
* @property {String Number} title-width 标题的宽度,单位rpx |
|||
* @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item> |
|||
*/ |
|||
export default { |
|||
name: 'u-cell-item', |
|||
emits: ["click"], |
|||
props: { |
|||
// 左侧图标名称(只能uView内置图标),或者图标src |
|||
icon: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 左侧标题 |
|||
title: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 右侧内容 |
|||
value: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 标题下方的描述信息 |
|||
label: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 是否显示下边框 |
|||
borderBottom: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示上边框 |
|||
borderTop: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 多个cell中,中间的cell显示下划线时,下划线是否给一个到左边的距离 |
|||
// 1.4.0版本废除此参数,默认边框由border-top和border-bottom提供,此参数会造成干扰 |
|||
// borderGap: { |
|||
// type: Boolean, |
|||
// default: true |
|||
// }, |
|||
// 是否开启点击反馈,即点击时cell背景为灰色,none为无效果 |
|||
hoverClass: { |
|||
type: String, |
|||
default: 'u-cell-hover' |
|||
}, |
|||
// 是否显示右侧箭头 |
|||
arrow: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 内容是否垂直居中 |
|||
center: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否显示左边表示必填的星号 |
|||
required: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 标题的宽度,单位rpx |
|||
titleWidth: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 右侧箭头方向,可选值:right|up|down,默认为right |
|||
arrowDirection: { |
|||
type: String, |
|||
default: 'right' |
|||
}, |
|||
// 控制标题的样式 |
|||
titleStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 右侧显示内容的样式 |
|||
valueStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 描述信息的样式 |
|||
labelStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: 'transparent' |
|||
}, |
|||
// 用于识别被点击的是第几个cell |
|||
index: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 是否使用lable插槽 |
|||
useLabelSlot: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 左边图标的大小,单位rpx,只对传入icon字段时有效 |
|||
iconSize: { |
|||
type: [Number, String], |
|||
default: 34 |
|||
}, |
|||
// 左边图标的样式,对象形式 |
|||
iconStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
|
|||
}; |
|||
}, |
|||
computed: { |
|||
arrowStyle() { |
|||
let style = {}; |
|||
if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)'; |
|||
else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)'; |
|||
else style.transform = 'rotate(0deg)'; |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
click() { |
|||
this.$emit('click', this.index); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
.u-cell { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
position: relative; |
|||
/* #ifndef APP-NVUE */ |
|||
box-sizing: border-box; |
|||
/* #endif */ |
|||
width: 100%; |
|||
padding: 26rpx 32rpx; |
|||
font-size: 28rpx; |
|||
line-height: 54rpx; |
|||
color: $u-content-color; |
|||
background-color: #fff; |
|||
text-align: left; |
|||
} |
|||
|
|||
.u-cell_title { |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.u-cell__left-icon-wrap { |
|||
margin-right: 10rpx; |
|||
font-size: 32rpx; |
|||
} |
|||
|
|||
.u-cell__right-icon-wrap { |
|||
margin-left: 10rpx; |
|||
color: #969799; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.u-cell__left-icon-wrap, |
|||
.u-cell__right-icon-wrap { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
height: 48rpx; |
|||
} |
|||
|
|||
.u-cell-border:after { |
|||
position: absolute; |
|||
/* #ifndef APP-NVUE */ |
|||
box-sizing: border-box; |
|||
content: ' '; |
|||
pointer-events: none; |
|||
border-bottom: 1px solid $u-border-color; |
|||
/* #endif */ |
|||
right: 0; |
|||
left: 0; |
|||
top: 0; |
|||
transform: scaleY(0.5); |
|||
} |
|||
|
|||
.u-cell-border { |
|||
position: relative; |
|||
} |
|||
|
|||
.u-cell__label { |
|||
margin-top: 6rpx; |
|||
font-size: 26rpx; |
|||
line-height: 36rpx; |
|||
color: $u-tips-color; |
|||
/* #ifndef APP-NVUE */ |
|||
word-wrap: break-word; |
|||
/* #endif */ |
|||
} |
|||
|
|||
.u-cell__value { |
|||
overflow: hidden; |
|||
text-align: right; |
|||
/* #ifndef APP-NVUE */ |
|||
vertical-align: middle; |
|||
/* #endif */ |
|||
color: $u-tips-color; |
|||
font-size: 26rpx; |
|||
} |
|||
|
|||
.u-cell__title, |
|||
.u-cell__value { |
|||
flex: 1; |
|||
} |
|||
|
|||
.u-cell--required { |
|||
/* #ifndef APP-NVUE */ |
|||
overflow: visible; |
|||
/* #endif */ |
|||
@include vue-flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-cell--required:before { |
|||
position: absolute; |
|||
/* #ifndef APP-NVUE */ |
|||
content: '*'; |
|||
/* #endif */ |
|||
left: 8px; |
|||
margin-top: 4rpx; |
|||
font-size: 14px; |
|||
color: $u-type-error; |
|||
} |
|||
|
|||
.u-cell_right { |
|||
line-height: 1; |
|||
} |
|||
</style> |
@ -0,0 +1,148 @@ |
|||
<template> |
|||
<view class="u-checkbox-group u-clearfix"> |
|||
<slot></slot> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import Emitter from '../../libs/util/emitter.js'; |
|||
/** |
|||
* checkboxGroup 开关选择器父组件Group |
|||
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便 |
|||
* @tutorial https://www.uviewui.com/components/checkbox.html |
|||
* @property {String Number} max 最多能选中多少个checkbox(默认999) |
|||
* @property {String Number} size 组件整体的大小,单位rpx(默认40) |
|||
* @property {Boolean} disabled 是否禁用所有checkbox(默认false) |
|||
* @property {String Number} icon-size 图标大小,单位rpx(默认20) |
|||
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false) |
|||
* @property {String} width 宽度,需带单位 |
|||
* @property {String} width 宽度,需带单位 |
|||
* @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle) |
|||
* @property {Boolean} wrap 是否每个checkbox都换行(默认false) |
|||
* @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认#2979ff) |
|||
* @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象 |
|||
* @example <u-checkbox-group></u-checkbox-group> |
|||
*/ |
|||
export default { |
|||
name: 'u-checkbox-group', |
|||
emits: ["update:modelValue", "input", "change"], |
|||
mixins: [Emitter], |
|||
props: { |
|||
// 匹配某一个radio组件,如果某个radio的name值等于此值,那么这个radio就被会选中 |
|||
value: { |
|||
type: [String, Number, Array, Boolean], |
|||
default: '' |
|||
}, |
|||
modelValue: { |
|||
type: [String, Number, Array, Boolean], |
|||
default: '' |
|||
}, |
|||
// 最多能选中多少个checkbox |
|||
max: { |
|||
type: [Number, String], |
|||
default: 999 |
|||
}, |
|||
// 所有选中项的 name |
|||
// value: { |
|||
// default: Array, |
|||
// default() { |
|||
// return [] |
|||
// } |
|||
// }, |
|||
// 是否禁用所有复选框 |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 在表单内提交时的标识符 |
|||
name: { |
|||
type: [Boolean, String], |
|||
default: '' |
|||
}, |
|||
// 是否禁止点击提示语选中复选框 |
|||
labelDisabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 形状,square为方形,circle为原型 |
|||
shape: { |
|||
type: String, |
|||
default: 'square' |
|||
}, |
|||
// 选中状态下的颜色 |
|||
activeColor: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
}, |
|||
// 组件的整体大小 |
|||
size: { |
|||
type: [String, Number], |
|||
default: 34 |
|||
}, |
|||
// 每个checkbox占u-checkbox-group的宽度 |
|||
width: { |
|||
type: String, |
|||
default: 'auto' |
|||
}, |
|||
// 是否每个checkbox都换行 |
|||
wrap: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 图标的大小,单位rpx |
|||
iconSize: { |
|||
type: [String, Number], |
|||
default: 20 |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
values:[] |
|||
} |
|||
}, |
|||
created() { |
|||
// 如果将children定义在data中,在微信小程序会造成循环引用而报错 |
|||
this.children = []; |
|||
}, |
|||
methods: { |
|||
emitEvent(obj) { |
|||
let values = this.values || []; |
|||
if(obj.value){ |
|||
let index = values.indexOf(obj.name); |
|||
if (index === -1) { |
|||
values.push(obj.name); |
|||
} |
|||
}else{ |
|||
let index = values.indexOf(obj.name); |
|||
if (index > -1) { |
|||
values.splice(index,1); |
|||
} |
|||
} |
|||
// this.children.map(val => { |
|||
// if(val.value) values.push(val.name); |
|||
// }) |
|||
this.$emit('change', values); |
|||
// 通过emit事件,设置父组件通过v-model双向绑定的值 |
|||
this.$emit('input', values); |
|||
this.$emit("update:modelValue", values); |
|||
// 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证 |
|||
// 由于头条小程序执行迟钝,故需要用几十毫秒的延时 |
|||
setTimeout(() => { |
|||
// 将当前的值发送到 u-form-item 进行校验 |
|||
this.dispatch('u-form-item', 'onFieldChange', values); |
|||
}, 60) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-checkbox-group { |
|||
/* #ifndef MP || APP-NVUE */ |
|||
display: inline-flex; |
|||
flex-wrap: wrap; |
|||
/* #endif */ |
|||
} |
|||
</style> |
@ -0,0 +1,299 @@ |
|||
<template> |
|||
<view class="u-checkbox" :style="[checkboxStyle]"> |
|||
<view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]"> |
|||
<u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor"/> |
|||
</view> |
|||
<view class="u-checkbox__label" @tap="onClickLabel" :style="{ |
|||
fontSize: $u.addUnit(labelSize) |
|||
}"> |
|||
<slot /> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* checkbox 复选框 |
|||
* @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。 |
|||
* @tutorial https://www.uviewui.com/components/checkbox.html |
|||
* @property {String Number} icon-size 图标大小,单位rpx(默认20) |
|||
* @property {String Number} label-size label字体大小,单位rpx(默认28) |
|||
* @property {String Number} name checkbox组件的标示符 |
|||
* @property {String} shape 形状,见官网说明(默认circle) |
|||
* @property {Boolean} disabled 是否禁用 |
|||
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox |
|||
* @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效 |
|||
* @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象 |
|||
* @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox> |
|||
*/ |
|||
export default { |
|||
name: "u-checkbox", |
|||
emits: ["update:modelValue", "input", "change"], |
|||
props: { |
|||
// 是否为选中状态 |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
modelValue: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// checkbox的名称 |
|||
name: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 形状,square为方形,circle为原型 |
|||
shape: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否禁用 |
|||
disabled: { |
|||
type: [String, Boolean], |
|||
default: '' |
|||
}, |
|||
// 是否禁止点击提示语选中复选框 |
|||
labelDisabled: { |
|||
type: [String, Boolean], |
|||
default: '' |
|||
}, |
|||
// 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值 |
|||
activeColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 图标的大小,单位rpx |
|||
iconSize: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// label的字体大小,rpx单位 |
|||
labelSize: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 组件的整体大小 |
|||
size: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
parentDisabled: false, |
|||
newParams: {}, |
|||
}; |
|||
}, |
|||
created() { |
|||
// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用 |
|||
this.parent = this.$u.$parent.call(this, 'u-checkbox-group'); |
|||
// 如果存在u-checkbox-group,将本组件的this塞进父组件的children中 |
|||
this.parent && this.parent.children.push(this); |
|||
}, |
|||
computed: { |
|||
// 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置 |
|||
isDisabled() { |
|||
return this.disabled !== '' ? this.disabled : this.parent ? this.parent.disabled : false; |
|||
}, |
|||
// 是否禁用label点击 |
|||
isLabelDisabled() { |
|||
return this.labelDisabled !== '' ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false; |
|||
}, |
|||
// 组件尺寸,对应size的值,默认值为34rpx |
|||
checkboxSize() { |
|||
return this.size ? this.size : (this.parent ? this.parent.size : 34); |
|||
}, |
|||
// 组件的勾选图标的尺寸,默认20 |
|||
checkboxIconSize() { |
|||
return this.iconSize ? this.iconSize : (this.parent ? this.parent.iconSize : 20); |
|||
}, |
|||
// 组件选中激活时的颜色 |
|||
elActiveColor() { |
|||
return this.activeColor ? this.activeColor : (this.parent ? this.parent.activeColor : 'primary'); |
|||
}, |
|||
// 组件的形状 |
|||
elShape() { |
|||
return this.shape ? this.shape : (this.parent ? this.parent.shape : 'square'); |
|||
}, |
|||
iconStyle() { |
|||
let style = {}; |
|||
// 既要判断是否手动禁用,还要判断用户v-model绑定的值,如果绑定为false,那么也无法选中 |
|||
if (this.elActiveColor && this.getValue() && !this.isDisabled) { |
|||
style.borderColor = this.elActiveColor; |
|||
style.backgroundColor = this.elActiveColor; |
|||
} |
|||
style.width = this.$u.addUnit(this.checkboxSize); |
|||
style.height = this.$u.addUnit(this.checkboxSize); |
|||
return style; |
|||
}, |
|||
// checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可 |
|||
iconColor() { |
|||
return this.getValue() ? '#ffffff' : 'transparent'; |
|||
}, |
|||
iconClass() { |
|||
let classes = []; |
|||
classes.push('u-checkbox__icon-wrap--' + this.elShape); |
|||
if (this.getValue() == true) classes.push('u-checkbox__icon-wrap--checked'); |
|||
if (this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled'); |
|||
if (this.getValue() && this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled--checked'); |
|||
// 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效 |
|||
return classes.join(' '); |
|||
}, |
|||
checkboxStyle() { |
|||
let style = {}; |
|||
if(this.parent && this.parent.width) { |
|||
style.width = this.parent.width; |
|||
// #ifdef MP |
|||
// 各家小程序因为它们特殊的编译结构,使用float布局 |
|||
style.float = 'left'; |
|||
// #endif |
|||
// #ifndef MP |
|||
// H5和APP使用flex布局 |
|||
style.flex = `0 0 ${this.parent.width}`; |
|||
// #endif |
|||
} |
|||
if(this.parent && this.parent.wrap) { |
|||
style.width = '100%'; |
|||
// #ifndef MP |
|||
// H5和APP使用flex布局,将宽度设置100%,即可自动换行 |
|||
style.flex = '0 0 100%'; |
|||
// #endif |
|||
} |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
getValue(){ |
|||
// #ifndef VUE3 |
|||
return this.value; |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
return this.modelValue; |
|||
// #endif |
|||
}, |
|||
onClickLabel() { |
|||
if (!this.isLabelDisabled && !this.isDisabled) { |
|||
this.setValue(); |
|||
} |
|||
}, |
|||
toggle() { |
|||
if (!this.isDisabled) { |
|||
this.setValue(); |
|||
} |
|||
}, |
|||
emitEvent() { |
|||
let obj = { |
|||
value: !this.getValue(), |
|||
name: this.name |
|||
}; |
|||
this.$emit('change', obj) |
|||
// 执行父组件u-checkbox-group的事件方法 |
|||
if(this.parent && this.parent.emitEvent) this.parent.emitEvent(obj); |
|||
}, |
|||
// 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值 |
|||
setValue() { |
|||
let value = this.getValue(); |
|||
// 判断是否超过了可选的最大数量 |
|||
let checkedNum = 0; |
|||
if(this.parent && this.parent.children) { |
|||
// 只要父组件的某一个子元素的value为true,就加1(已有的选中数量) |
|||
this.parent.children.map(val => { |
|||
if (val.value) checkedNum++; |
|||
}) |
|||
} |
|||
// 如果原来为选中状态,那么可以取消 |
|||
if (value == true) { |
|||
this.emitEvent(); |
|||
this.$emit('input', !value); |
|||
this.$emit('update:modelValue', !value); |
|||
} else { |
|||
// 如果超出最多可选项,提示 |
|||
if(this.parent && checkedNum >= this.parent.max) { |
|||
return this.$u.toast(`最多可选${this.parent.max}项`); |
|||
} |
|||
// 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中 |
|||
this.emitEvent(); |
|||
this.$emit('input', !value); |
|||
this.$emit('update:modelValue', !value); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-checkbox { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
overflow: hidden; |
|||
user-select: none; |
|||
line-height: 1.8; |
|||
|
|||
&__icon-wrap { |
|||
color: $u-content-color; |
|||
flex: none; |
|||
display: -webkit-flex; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
box-sizing: border-box; |
|||
width: 42rpx; |
|||
height: 42rpx; |
|||
color: transparent; |
|||
text-align: center; |
|||
transition-property: color, border-color, background-color; |
|||
font-size: 20px; |
|||
border: 1px solid #c8c9cc; |
|||
transition-duration: 0.2s; |
|||
|
|||
/* #ifdef MP-TOUTIAO */ |
|||
// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下 |
|||
&__icon { |
|||
line-height: 0; |
|||
} |
|||
/* #endif */ |
|||
|
|||
&--circle { |
|||
border-radius: 100%; |
|||
} |
|||
|
|||
&--square { |
|||
border-radius: 6rpx; |
|||
} |
|||
|
|||
&--checked { |
|||
color: #fff; |
|||
background-color: $u-type-primary; |
|||
border-color: $u-type-primary; |
|||
} |
|||
|
|||
&--disabled { |
|||
background-color: #ebedf0; |
|||
border-color: #c8c9cc; |
|||
} |
|||
|
|||
&--disabled--checked { |
|||
color: #c8c9cc !important; |
|||
} |
|||
} |
|||
|
|||
&__label { |
|||
word-wrap: break-word; |
|||
margin-left: 10rpx; |
|||
margin-right: 24rpx; |
|||
color: $u-content-color; |
|||
font-size: 30rpx; |
|||
|
|||
&--disabled { |
|||
color: #c8c9cc; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,220 @@ |
|||
<template> |
|||
<view |
|||
class="u-circle-progress" |
|||
:style="{ |
|||
width: widthPx + 'px', |
|||
height: widthPx + 'px', |
|||
backgroundColor: bgColor |
|||
}" |
|||
> |
|||
<!-- 支付宝小程序不支持canvas-id属性,必须用id属性 --> |
|||
<canvas |
|||
class="u-canvas-bg" |
|||
:canvas-id="elBgId" |
|||
:id="elBgId" |
|||
:style="{ |
|||
width: widthPx + 'px', |
|||
height: widthPx + 'px' |
|||
}" |
|||
></canvas> |
|||
<canvas |
|||
class="u-canvas" |
|||
:canvas-id="elId" |
|||
:id="elId" |
|||
:style="{ |
|||
width: widthPx + 'px', |
|||
height: widthPx + 'px' |
|||
}" |
|||
></canvas> |
|||
<slot></slot> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* circleProgress 环形进度条 |
|||
* @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。 |
|||
* @tutorial https://www.uviewui.com/components/circleProgress.html |
|||
* @property {String Number} percent 圆环进度百分比值,为数值类型,0-100 |
|||
* @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec) |
|||
* @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b) |
|||
* @property {String Number} width 整个圆环组件的宽度,高度默认等于宽度值,单位rpx(默认200) |
|||
* @property {String Number} border-width 圆环的边框宽度,单位rpx(默认14) |
|||
* @property {String Number} duration 整个圆环执行一圈的时间,单位ms(默认呢1500) |
|||
* @property {String} type 如设置,active-color值将会失效 |
|||
* @property {String} bg-color 整个组件背景颜色,默认为白色 |
|||
* @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress> |
|||
*/ |
|||
export default { |
|||
name: 'u-circle-progress', |
|||
props: { |
|||
// 圆环进度百分比值 |
|||
percent: { |
|||
type: Number, |
|||
default: 0, |
|||
// 限制值在0到100之间 |
|||
validator: val => { |
|||
return val >= 0 && val <= 100; |
|||
} |
|||
}, |
|||
// 底部圆环的颜色(灰色的圆环) |
|||
inactiveColor: { |
|||
type: String, |
|||
default: '#ececec' |
|||
}, |
|||
// 圆环激活部分的颜色 |
|||
activeColor: { |
|||
type: String, |
|||
default: '#19be6b' |
|||
}, |
|||
// 圆环线条的宽度,单位rpx |
|||
borderWidth: { |
|||
type: [Number, String], |
|||
default: 14 |
|||
}, |
|||
// 整个圆形的宽度,单位rpx |
|||
width: { |
|||
type: [Number, String], |
|||
default: 200 |
|||
}, |
|||
// 整个圆环执行一圈的时间,单位ms |
|||
duration: { |
|||
type: [Number, String], |
|||
default: 1500 |
|||
}, |
|||
// 主题类型 |
|||
type: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 整个圆环进度区域的背景色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '#ffffff' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
// #ifdef MP-WEIXIN |
|||
elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值,否则会报错 |
|||
elId: 'uCircleProgressElId', |
|||
// #endif |
|||
// #ifndef MP-WEIXIN |
|||
elBgId: this.$u.guid(), // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱 |
|||
elId: this.$u.guid(), |
|||
// #endif |
|||
widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度 |
|||
borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度 |
|||
startAngle: -Math.PI / 2, // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向 |
|||
progressContext: null, // 活动圆的canvas上下文 |
|||
newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用 |
|||
oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用 |
|||
}; |
|||
}, |
|||
watch: { |
|||
percent(nVal, oVal = 0) { |
|||
if (nVal > 100) nVal = 100; |
|||
if (nVal < 0) oVal = 0; |
|||
// 此值其实等于this.percent,命名一个新 |
|||
this.newPercent = nVal; |
|||
this.oldPercent = oVal; |
|||
setTimeout(() => { |
|||
// 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值 |
|||
// 将此值减少或者新增到新的百分比值 |
|||
this.drawCircleByProgress(oVal); |
|||
}, 50); |
|||
} |
|||
}, |
|||
created() { |
|||
// 赋值,用于加载后第一个画圆使用 |
|||
this.newPercent = this.percent; |
|||
this.oldPercent = 0; |
|||
}, |
|||
computed: { |
|||
// 有type主题时,优先起作用 |
|||
circleColor() { |
|||
if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type]; |
|||
else return this.activeColor; |
|||
} |
|||
}, |
|||
mounted() { |
|||
// 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7) |
|||
setTimeout(() => { |
|||
this.drawProgressBg(); |
|||
this.drawCircleByProgress(this.oldPercent); |
|||
}, 50); |
|||
}, |
|||
methods: { |
|||
drawProgressBg() { |
|||
let ctx = uni.createCanvasContext(this.elBgId, this); |
|||
ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度 |
|||
ctx.setStrokeStyle(this.inactiveColor); // 线条颜色 |
|||
ctx.beginPath(); // 开始描绘路径 |
|||
// 设置一个原点(110,110),半径为100的圆的路径到当前路径 |
|||
let radius = this.widthPx / 2; |
|||
ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false); |
|||
ctx.stroke(); // 对路径进行描绘 |
|||
ctx.draw(); |
|||
}, |
|||
drawCircleByProgress(progress) { |
|||
// 第一次操作进度环时将上下文保存到了this.data中,直接使用即可 |
|||
let ctx = this.progressContext; |
|||
if (!ctx) { |
|||
ctx = uni.createCanvasContext(this.elId, this); |
|||
this.progressContext = ctx; |
|||
} |
|||
// 表示进度的两端为圆形 |
|||
ctx.setLineCap('round'); |
|||
// 设置线条的宽度和颜色 |
|||
ctx.setLineWidth(this.borderWidthPx); |
|||
ctx.setStrokeStyle(this.circleColor); |
|||
// 将总过渡时间除以100,得出每修改百分之一进度所需的时间 |
|||
let time = Math.floor(this.duration / 100); |
|||
// 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的 |
|||
// 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值 |
|||
let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle; |
|||
ctx.beginPath(); |
|||
// 半径为整个canvas宽度的一半 |
|||
let radius = this.widthPx / 2; |
|||
ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false); |
|||
ctx.stroke(); |
|||
ctx.draw(); |
|||
// 如果变更后新值大于旧值,意味着增大了百分比 |
|||
if (this.newPercent > this.oldPercent) { |
|||
// 每次递增百分之一 |
|||
progress++; |
|||
// 如果新增后的值,大于需要设置的值百分比值,停止继续增加 |
|||
if (progress > this.newPercent) return; |
|||
} else { |
|||
// 同理于上面 |
|||
progress--; |
|||
if (progress < this.newPercent) return; |
|||
} |
|||
setTimeout(() => { |
|||
// 定时器,每次操作间隔为time值,为了让进度条有动画效果 |
|||
this.drawCircleByProgress(progress); |
|||
}, time); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
.u-circle-progress { |
|||
position: relative; |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.u-canvas-bg { |
|||
position: absolute; |
|||
} |
|||
|
|||
.u-canvas { |
|||
position: absolute; |
|||
} |
|||
</style> |
@ -0,0 +1,156 @@ |
|||
<template> |
|||
<view class="u-col" :class="[ |
|||
'u-col-' + span |
|||
]" :style="{ |
|||
padding: `0 ${Number(gutter)/2 + 'rpx'}`, |
|||
marginLeft: 100 / 12 * offset + '%', |
|||
flex: `0 0 ${100 / 12 * span}%`, |
|||
alignItems: uAlignItem, |
|||
justifyContent: uJustify, |
|||
textAlign: textAlign |
|||
}" |
|||
@tap="click"> |
|||
<slot></slot> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* col 布局单元格 |
|||
* @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用) |
|||
* @tutorial https://www.uviewui.com/components/layout.html |
|||
* @property {String Number} span 栅格占据的列数,总12等分(默认0) |
|||
* @property {String} text-align 文字水平对齐方式(默认left) |
|||
* @property {String Number} offset 分栏左边偏移,计算方式与span相同(默认0) |
|||
* @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col> |
|||
*/ |
|||
export default { |
|||
name: "u-col", |
|||
props: { |
|||
// 占父容器宽度的多少等分,总分为12份 |
|||
span: { |
|||
type: [Number, String], |
|||
default: 12 |
|||
}, |
|||
// 指定栅格左侧的间隔数(总12栏) |
|||
offset: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`) |
|||
justify: { |
|||
type: String, |
|||
default: 'start' |
|||
}, |
|||
// 垂直对齐方式,可选值为top、center、bottom |
|||
align: { |
|||
type: String, |
|||
default: 'center' |
|||
}, |
|||
// 文字对齐方式 |
|||
textAlign: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// 是否阻止事件传播 |
|||
stop: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
gutter: 20, // 给col添加间距,左右边距各占一半,从父组件u-row获取 |
|||
} |
|||
}, |
|||
created() { |
|||
this.parent = false; |
|||
}, |
|||
mounted() { |
|||
// 获取父组件实例,并赋值给对应的参数 |
|||
this.parent = this.$u.$parent.call(this, 'u-row'); |
|||
if (this.parent) { |
|||
this.gutter = this.parent.gutter; |
|||
} |
|||
}, |
|||
computed: { |
|||
uJustify() { |
|||
if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify; |
|||
else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify; |
|||
else return this.justify; |
|||
}, |
|||
uAlignItem() { |
|||
if (this.align == 'top') return 'flex-start'; |
|||
if (this.align == 'bottom') return 'flex-end'; |
|||
else return this.align; |
|||
} |
|||
}, |
|||
methods: { |
|||
click(e) { |
|||
this.$emit('click'); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-col { |
|||
/* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */ |
|||
float: left; |
|||
/* #endif */ |
|||
} |
|||
|
|||
.u-col-0 { |
|||
width: 0; |
|||
} |
|||
|
|||
.u-col-1 { |
|||
width: calc(100%/12); |
|||
} |
|||
|
|||
.u-col-2 { |
|||
width: calc(100%/12 * 2); |
|||
} |
|||
|
|||
.u-col-3 { |
|||
width: calc(100%/12 * 3); |
|||
} |
|||
|
|||
.u-col-4 { |
|||
width: calc(100%/12 * 4); |
|||
} |
|||
|
|||
.u-col-5 { |
|||
width: calc(100%/12 * 5); |
|||
} |
|||
|
|||
.u-col-6 { |
|||
width: calc(100%/12 * 6); |
|||
} |
|||
|
|||
.u-col-7 { |
|||
width: calc(100%/12 * 7); |
|||
} |
|||
|
|||
.u-col-8 { |
|||
width: calc(100%/12 * 8); |
|||
} |
|||
|
|||
.u-col-9 { |
|||
width: calc(100%/12 * 9); |
|||
} |
|||
|
|||
.u-col-10 { |
|||
width: calc(100%/12 * 10); |
|||
} |
|||
|
|||
.u-col-11 { |
|||
width: calc(100%/12 * 11); |
|||
} |
|||
|
|||
.u-col-12 { |
|||
width: calc(100%/12 * 12); |
|||
} |
|||
</style> |
@ -0,0 +1,205 @@ |
|||
<template> |
|||
<view class="u-collapse-item" :style="[itemStyle]"> |
|||
<view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]"> |
|||
<block v-if="!$slots['title-all']"> |
|||
<view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' }, |
|||
isShow && activeStyle && !arrow ? activeStyle : '']"> |
|||
{{ title }} |
|||
</view> |
|||
<slot v-else name="title" /> |
|||
<view class="u-icon-wrap"> |
|||
<u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }" |
|||
class="u-arrow-down-icon" name="arrow-down"></u-icon> |
|||
</view> |
|||
</block> |
|||
<slot v-else name="title-all" /> |
|||
</view> |
|||
<view class="u-collapse-body" :style="[{ |
|||
height: isShow ? height + 'px' : '0' |
|||
}]"> |
|||
<view class="u-collapse-content" :id="elId" :style="[bodyStyle]"> |
|||
<slot></slot> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* collapseItem 手风琴Item |
|||
* @description 通过折叠面板收纳内容区域(搭配u-collapse使用) |
|||
* @tutorial https://www.uviewui.com/components/collapse.html |
|||
* @property {String} title 面板标题 |
|||
* @property {String Number} index 主要用于事件的回调,标识那个Item被点击 |
|||
* @property {Boolean} disabled 面板是否可以打开或收起(默认false) |
|||
* @property {Boolean} open 设置某个面板的初始状态是否打开(默认false) |
|||
* @property {String Number} name 唯一标识符,如不设置,默认用当前collapse-item的索引值 |
|||
* @property {String} align 标题的对齐方式(默认left) |
|||
* @property {Object} active-style 不显示箭头时,可以添加当前选择的collapse-item活动样式,对象形式 |
|||
* @event {Function} change 某个item被打开或者收起时触发 |
|||
* @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item> |
|||
*/ |
|||
export default { |
|||
name: "u-collapse-item", |
|||
emits: ["change"], |
|||
props: { |
|||
// 标题 |
|||
title: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 标题的对齐方式 |
|||
align: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// 是否可以点击收起 |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// collapse显示与否 |
|||
open: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 唯一标识符 |
|||
name: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
//活动样式 |
|||
activeStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 标识当前为第几个 |
|||
index: { |
|||
type: [String, Number], |
|||
default: '' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
isShow: false, |
|||
elId: this.$u.guid(), |
|||
height: 0, // body内容的高度 |
|||
headStyle: {}, // 头部样式,对象形式 |
|||
bodyStyle: {}, // 主体部分样式 |
|||
itemStyle: {}, // 每个item的整体样式 |
|||
arrowColor: '', // 箭头的颜色 |
|||
hoverClass: '', // 头部按下时的效果样式类 |
|||
arrow: true, // 是否显示右侧箭头 |
|||
|
|||
}; |
|||
}, |
|||
watch: { |
|||
open(val) { |
|||
this.isShow = val; |
|||
} |
|||
}, |
|||
created() { |
|||
this.parent = false; |
|||
// 获取u-collapse的信息,放在u-collapse是为了方便,不用每个u-collapse-item写一遍 |
|||
this.isShow = this.open; |
|||
}, |
|||
methods: { |
|||
// 异步获取内容,或者动态修改了内容时,需要重新初始化 |
|||
init() { |
|||
this.parent = this.$u.$parent.call(this, 'u-collapse'); |
|||
if(this.parent) { |
|||
this.nameSync = this.name ? this.name : this.parent.childrens.length; |
|||
this.parent.childrens.push(this); |
|||
this.headStyle = this.parent.headStyle; |
|||
this.bodyStyle = this.parent.bodyStyle; |
|||
this.arrowColor = this.parent.arrowColor; |
|||
this.hoverClass = this.parent.hoverClass; |
|||
this.arrow = this.parent.arrow; |
|||
this.itemStyle = this.parent.itemStyle; |
|||
} |
|||
this.$nextTick(() => { |
|||
this.queryRect(); |
|||
}); |
|||
}, |
|||
// 点击collapsehead头部 |
|||
headClick() { |
|||
if (this.disabled) return; |
|||
if (this.parent && this.parent.accordion == true) { |
|||
this.parent.childrens.map(val => { |
|||
// 自身不设置为false,因为后面有this.isShow = !this.isShow;处理了 |
|||
if (this != val) { |
|||
val.isShow = false; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
this.isShow = !this.isShow; |
|||
// 触发本组件的事件 |
|||
this.$emit('change', { |
|||
index: this.index, |
|||
show: this.isShow |
|||
}) |
|||
// 只有在打开时才发出事件 |
|||
if (this.isShow) this.parent && this.parent.onChange(); |
|||
this.$forceUpdate(); |
|||
}, |
|||
// 查询内容高度 |
|||
queryRect() { |
|||
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html |
|||
// 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同 |
|||
this.$uGetRect('#' + this.elId).then(res => { |
|||
this.height = res.height; |
|||
}) |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.init(); |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-collapse-head { |
|||
position: relative; |
|||
@include vue-flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
color: $u-main-color; |
|||
font-size: 30rpx; |
|||
line-height: 1; |
|||
padding: 24rpx 0; |
|||
text-align: left; |
|||
} |
|||
|
|||
.u-collapse-title { |
|||
flex: 1; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.u-arrow-down-icon { |
|||
transition: all 0.3s; |
|||
margin-right: 20rpx; |
|||
margin-left: 14rpx; |
|||
} |
|||
|
|||
.u-arrow-down-icon-active { |
|||
transform: rotate(180deg); |
|||
transform-origin: center center; |
|||
} |
|||
|
|||
.u-collapse-body { |
|||
overflow: hidden; |
|||
transition: all 0.3s; |
|||
} |
|||
|
|||
.u-collapse-content { |
|||
overflow: hidden; |
|||
font-size: 28rpx; |
|||
color: $u-tips-color; |
|||
text-align: left; |
|||
} |
|||
</style> |
@ -0,0 +1,100 @@ |
|||
<template> |
|||
<view class="u-collapse"> |
|||
<slot /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* collapse 手风琴 |
|||
* @description 通过折叠面板收纳内容区域 |
|||
* @tutorial https://www.uviewui.com/components/collapse.html |
|||
* @property {Boolean} accordion 是否手风琴模式(默认true) |
|||
* @property {Boolean} arrow 是否显示标题右侧的箭头(默认true) |
|||
* @property {String} arrow-color 标题右侧箭头的颜色(默认#909399) |
|||
* @property {Object} head-style 标题自定义样式,对象形式 |
|||
* @property {Object} body-style 主体自定义样式,对象形式 |
|||
* @property {String} hover-class 样式类名,按下时有效(默认u-hover-class) |
|||
* @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array) |
|||
* @example <u-collapse></u-collapse> |
|||
*/ |
|||
export default { |
|||
name:"u-collapse", |
|||
emits: ["change"], |
|||
props: { |
|||
// 是否手风琴模式 |
|||
accordion: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 头部的样式 |
|||
headStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 主体的样式 |
|||
bodyStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 每一个item的样式 |
|||
itemStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 是否显示右侧的箭头 |
|||
arrow: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 箭头的颜色 |
|||
arrowColor: { |
|||
type: String, |
|||
default: '#909399' |
|||
}, |
|||
// 标题部分按压时的样式类,"none"为无效果 |
|||
hoverClass: { |
|||
type: String, |
|||
default: 'u-hover-class' |
|||
} |
|||
}, |
|||
created() { |
|||
this.childrens = [] |
|||
}, |
|||
data() { |
|||
return { |
|||
|
|||
} |
|||
}, |
|||
methods: { |
|||
// 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况 |
|||
init() { |
|||
this.childrens.forEach((vm, index) => { |
|||
vm.init(); |
|||
}) |
|||
}, |
|||
// collapse item被点击,由collapse item调用父组件方法 |
|||
onChange() { |
|||
let activeItem = []; |
|||
this.childrens.forEach((vm, index) => { |
|||
if (vm.isShow) { |
|||
activeItem.push(vm.nameSync); |
|||
} |
|||
}) |
|||
// 如果是手风琴模式,只有一个匹配结果,也即activeItem长度为1,将其转为字符串 |
|||
if (this.accordion) activeItem = activeItem.join(''); |
|||
this.$emit('change', activeItem); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
</style> |
@ -0,0 +1,238 @@ |
|||
<template> |
|||
<view |
|||
class="u-notice-bar" |
|||
:style="{ |
|||
background: computeBgColor, |
|||
padding: padding |
|||
}" |
|||
:class="[ |
|||
type ? `u-type-${type}-light-bg` : '' |
|||
]" |
|||
> |
|||
<view class="u-icon-wrap"> |
|||
<u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon> |
|||
</view> |
|||
<swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper"> |
|||
<swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item"> |
|||
<view |
|||
class="u-news-item u-line-1" |
|||
:style="[textStyle]" |
|||
@tap="click(index)" |
|||
:class="['u-type-' + type]" |
|||
> |
|||
{{ item }} |
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<view class="u-icon-wrap"> |
|||
<u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon> |
|||
<u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
emits: ["close", "getMore", "end"], |
|||
props: { |
|||
// 显示的内容,数组 |
|||
list: { |
|||
type: Array, |
|||
default() { |
|||
return []; |
|||
} |
|||
}, |
|||
// 显示的主题,success|error|primary|info|warning |
|||
type: { |
|||
type: String, |
|||
default: 'warning' |
|||
}, |
|||
// 是否显示左侧的音量图标 |
|||
volumeIcon: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示右侧的右箭头图标 |
|||
moreIcon: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否显示右侧的关闭图标 |
|||
closeIcon: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否自动播放 |
|||
autoplay: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 文字颜色,各图标也会使用文字颜色 |
|||
color: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 滚动方向,row-水平滚动,column-垂直滚动 |
|||
direction: { |
|||
type: String, |
|||
default: 'row' |
|||
}, |
|||
// 是否显示 |
|||
show: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 字体大小,单位rpx |
|||
fontSize: { |
|||
type: [Number, String], |
|||
default: 26 |
|||
}, |
|||
// 滚动一个周期的时间长,单位ms |
|||
duration: { |
|||
type: [Number, String], |
|||
default: 2000 |
|||
}, |
|||
// 音量喇叭的大小 |
|||
volumeSize: { |
|||
type: [Number, String], |
|||
default: 34 |
|||
}, |
|||
// 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度 |
|||
speed: { |
|||
type: Number, |
|||
default: 160 |
|||
}, |
|||
// 水平滚动时,是否采用衔接形式滚动 |
|||
isCircular: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 滚动方向,horizontal-水平滚动,vertical-垂直滚动 |
|||
mode: { |
|||
type: String, |
|||
default: 'horizontal' |
|||
}, |
|||
// 播放状态,play-播放,paused-暂停 |
|||
playState: { |
|||
type: String, |
|||
default: 'play' |
|||
}, |
|||
// 是否禁止用手滑动切换 |
|||
// 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序 |
|||
disableTouch: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 通知的边距 |
|||
padding: { |
|||
type: [Number, String], |
|||
default: '18rpx 24rpx' |
|||
} |
|||
}, |
|||
computed: { |
|||
// 计算字体颜色,如果没有自定义的,就用uview主题颜色 |
|||
computeColor() { |
|||
if (this.color) return this.color; |
|||
// 如果是无主题,就默认使用content-color |
|||
else if(this.type == 'none') return '#606266'; |
|||
else return this.type; |
|||
}, |
|||
// 文字内容的样式 |
|||
textStyle() { |
|||
let style = {}; |
|||
if (this.color) style.color = this.color; |
|||
else if(this.type == 'none') style.color = '#606266'; |
|||
style.fontSize = this.fontSize + 'rpx'; |
|||
return style; |
|||
}, |
|||
// 垂直或者水平滚动 |
|||
vertical() { |
|||
if(this.mode == 'horizontal') return false; |
|||
else return true; |
|||
}, |
|||
// 计算背景颜色 |
|||
computeBgColor() { |
|||
if (this.bgColor) return this.bgColor; |
|||
else if(this.type == 'none') return 'transparent'; |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
// animation: false |
|||
}; |
|||
}, |
|||
methods: { |
|||
// 点击通告栏 |
|||
click(index) { |
|||
this.$emit('click', index); |
|||
}, |
|||
// 点击关闭按钮 |
|||
close() { |
|||
this.$emit('close'); |
|||
}, |
|||
// 点击更多箭头按钮 |
|||
getMore() { |
|||
this.$emit('getMore'); |
|||
}, |
|||
change(e) { |
|||
let index = e.detail.current; |
|||
if(index == this.list.length - 1) { |
|||
this.$emit('end'); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-notice-bar { |
|||
width: 100%; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-wrap: nowrap; |
|||
padding: 18rpx 24rpx; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.u-swiper { |
|||
font-size: 26rpx; |
|||
height: 32rpx; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
margin-left: 12rpx; |
|||
} |
|||
|
|||
.u-swiper-item { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.u-news-item { |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.u-right-icon { |
|||
margin-left: 12rpx; |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-left-icon { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
} |
|||
</style> |
@ -0,0 +1,175 @@ |
|||
<template> |
|||
<view class="u-count-down"> |
|||
<slot> |
|||
<text class="u-count-down__text" :style="customStyle">{{ formattedTime }}</text> |
|||
</slot> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { isSameSecond, parseFormat, parseTimeData } from "./utils"; |
|||
/** |
|||
* u-count-down 倒计时 |
|||
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。 |
|||
* @tutorial https://uviewui.com/components/countDown.html |
|||
* @property {String | Number} timestamp 倒计时时长,单位ms (默认 0 ) |
|||
* @property {String} format 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 (默认 'HH:mm:ss' ) |
|||
* @property {Boolean} autoStart 是否自动开始倒计时 (默认 true ) |
|||
* @event {Function} end 倒计时结束时触发 |
|||
* @event {Function} change 倒计时变化时触发 |
|||
* @event {Function} start 开始倒计时 |
|||
* @event {Function} pause 暂停倒计时 |
|||
* @event {Function} reset 重设倒计时,若 auto-start 为 true,重设后会自动开始倒计时 |
|||
* @example <u-count-down :timestamp="timestamp"></u-count-down> |
|||
*/ |
|||
export default { |
|||
name: "u-count-down", |
|||
emits: ["change", "end", "finish"], |
|||
props: { |
|||
// 倒计时时长,单位ms |
|||
timestamp: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 |
|||
format: { |
|||
type: String, |
|||
default: "DD:HH:mm:ss" |
|||
}, |
|||
// 是否自动开始倒计时 |
|||
autoStart: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
customStyle: { |
|||
type: [String, Object], |
|||
default: "" |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
timer: null, |
|||
// 各单位(天,时,分等)剩余时间 |
|||
timeData: parseTimeData(0), |
|||
// 格式化后的时间,如"03:23:21" |
|||
formattedTime: "0", |
|||
// 倒计时是否正在进行中 |
|||
runing: false, |
|||
endTime: 0, // 结束的毫秒时间戳 |
|||
remainTime: 0 // 剩余的毫秒时间 |
|||
}; |
|||
}, |
|||
watch: { |
|||
timestamp(n) { |
|||
this.reset(); |
|||
}, |
|||
format(newVal, oldVal) { |
|||
this.pause(); |
|||
this.start(); |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.init(); |
|||
}, |
|||
methods: { |
|||
init() { |
|||
this.reset(); |
|||
}, |
|||
// 开始倒计时 |
|||
start() { |
|||
if (this.runing) return; |
|||
// 标识为进行中 |
|||
this.runing = true; |
|||
// 结束时间戳 = 此刻时间戳 + 剩余的时间 |
|||
this.endTime = Date.now() + this.remainTime; |
|||
this.toTick(); |
|||
}, |
|||
// 根据是否展示毫秒,执行不同操作函数 |
|||
toTick() { |
|||
if (this.format.indexOf("SSS") > -1) { |
|||
this.microTick(); |
|||
} else { |
|||
this.macroTick(); |
|||
} |
|||
}, |
|||
macroTick() { |
|||
this.clearTimeout(); |
|||
// 每隔一定时间,更新一遍定时器的值 |
|||
// 同时此定时器的作用也能带来毫秒级的更新 |
|||
this.timer = setTimeout(() => { |
|||
// 获取剩余时间 |
|||
const remain = this.getRemainTime(); |
|||
// 重设剩余时间 |
|||
if (!isSameSecond(remain, this.remainTime) || remain === 0) { |
|||
this.setRemainTime(remain); |
|||
} |
|||
// 如果剩余时间不为0,则继续检查更新倒计时 |
|||
if (this.remainTime !== 0) { |
|||
this.macroTick(); |
|||
} |
|||
}, 30); |
|||
}, |
|||
microTick() { |
|||
this.clearTimeout(); |
|||
this.timer = setTimeout(() => { |
|||
this.setRemainTime(this.getRemainTime()); |
|||
if (this.remainTime !== 0) { |
|||
this.microTick(); |
|||
} |
|||
}, 30); |
|||
}, |
|||
// 获取剩余的时间 |
|||
getRemainTime() { |
|||
// 取最大值,防止出现小于0的剩余时间值 |
|||
return Math.max(this.endTime - Date.now(), 0); |
|||
}, |
|||
// 设置剩余的时间 |
|||
setRemainTime(remain) { |
|||
this.remainTime = remain; |
|||
// 根据剩余的毫秒时间,得出该有天,小时,分钟等的值,返回一个对象 |
|||
const timeData = parseTimeData(remain); |
|||
this.$emit("change", timeData); |
|||
// 得出格式化后的时间 |
|||
this.formattedTime = parseFormat(this.format, timeData); |
|||
// 如果时间已到,停止倒计时 |
|||
if (remain <= 0) { |
|||
this.pause(); |
|||
this.$emit("end"); |
|||
this.$emit("finish"); |
|||
} |
|||
}, |
|||
// 重置倒计时 |
|||
reset() { |
|||
this.pause(); |
|||
this.remainTime = this.timestamp; |
|||
this.setRemainTime(this.remainTime); |
|||
if (this.autoStart) { |
|||
this.start(); |
|||
} |
|||
}, |
|||
// 暂停倒计时 |
|||
pause() { |
|||
this.runing = false; |
|||
this.clearTimeout(); |
|||
}, |
|||
// 清空定时器 |
|||
clearTimeout() { |
|||
clearTimeout(this.timer); |
|||
this.timer = null; |
|||
} |
|||
}, |
|||
// #ifndef VUE3 |
|||
beforeDestroy() { |
|||
this.clearTimeout(); |
|||
}, |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
beforeUnmount() { |
|||
this.clearTimeout(); |
|||
}, |
|||
// #endif |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss"></style> |
@ -0,0 +1,62 @@ |
|||
// 补0,如1 -> 01
|
|||
function padZero(num, targetLength = 2) { |
|||
let str = `${num}` |
|||
while (str.length < targetLength) { |
|||
str = `0${str}` |
|||
} |
|||
return str |
|||
} |
|||
const SECOND = 1000 |
|||
const MINUTE = 60 * SECOND |
|||
const HOUR = 60 * MINUTE |
|||
const DAY = 24 * HOUR |
|||
export function parseTimeData(time) { |
|||
const days = Math.floor(time / DAY) |
|||
const hours = Math.floor((time % DAY) / HOUR) |
|||
const minutes = Math.floor((time % HOUR) / MINUTE) |
|||
const seconds = Math.floor((time % MINUTE) / SECOND) |
|||
const milliseconds = Math.floor(time % SECOND) |
|||
return { |
|||
days, |
|||
hours, |
|||
minutes, |
|||
seconds, |
|||
milliseconds |
|||
} |
|||
} |
|||
export function parseFormat(format, timeData) { |
|||
let { |
|||
days, |
|||
hours, |
|||
minutes, |
|||
seconds, |
|||
milliseconds |
|||
} = timeData |
|||
// 如果格式化字符串中不存在DD(天),则将天的时间转为小时中去
|
|||
if (format.indexOf('DD') === -1) { |
|||
hours += days * 24 |
|||
} else { |
|||
// 对天补0
|
|||
format = format.replace('DD', padZero(days)) |
|||
} |
|||
// 其他同理于DD的格式化处理方式
|
|||
if (format.indexOf('HH') === -1) { |
|||
minutes += hours * 60 |
|||
} else { |
|||
format = format.replace('HH', padZero(hours)) |
|||
} |
|||
if (format.indexOf('mm') === -1) { |
|||
seconds += minutes * 60 |
|||
} else { |
|||
format = format.replace('mm', padZero(minutes)) |
|||
} |
|||
if (format.indexOf('ss') === -1) { |
|||
milliseconds += seconds * 1000 |
|||
} else { |
|||
format = format.replace('ss', padZero(seconds)) |
|||
} |
|||
return format.replace('SSS', padZero(milliseconds, 3)) |
|||
} |
|||
export function isSameSecond(time1, time2) { |
|||
return Math.floor(time1 / 1000) === Math.floor(time2 / 1000) |
|||
} |
@ -0,0 +1,266 @@ |
|||
<template> |
|||
<view |
|||
class="u-count-num" |
|||
:style="{ |
|||
fontSize: fontSize + 'rpx', |
|||
fontWeight: bold ? 'bold' : 'normal', |
|||
color: color |
|||
}" |
|||
> |
|||
{{ displayValueCom }} |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* countTo 数字滚动 |
|||
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。 |
|||
* @tutorial https://www.uviewui.com/components/countTo.html |
|||
* @property {String Number} nullVal 空值或NaN时显示的值,默认 - |
|||
* @property {String Number} start-val 开始值 |
|||
* @property {String Number} end-val 结束值 |
|||
* @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000) |
|||
* @property {Boolean} autoplay 是否自动开始滚动(默认true) |
|||
* @property {String Number} decimals 要显示的小数位数,见官网说明(默认0) |
|||
* @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true) |
|||
* @property {String} separator 千位分隔符,见官网说明 |
|||
* @property {String} color 字体颜色(默认#303133) |
|||
* @property {String Number} font-size 字体大小,单位rpx(默认50) |
|||
* @property {Boolean} bold 字体是否加粗(默认false) |
|||
* @event {Function} end 数值滚动到目标值时触发 |
|||
* @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to> |
|||
*/ |
|||
export default { |
|||
name: "u-count-to", |
|||
emits: ["end"], |
|||
props: { |
|||
// 没有值时显示 |
|||
nullVal: { |
|||
type: [Number, String], |
|||
default: "-" |
|||
}, |
|||
// 开始的数值,默认从0增长到某一个数 |
|||
startVal: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 要滚动的目标数值,必须 |
|||
endVal: { |
|||
type: [Number, String], |
|||
default: 0, |
|||
required: true |
|||
}, |
|||
// 滚动到目标数值的动画持续时间,单位为毫秒(ms) |
|||
duration: { |
|||
type: [Number, String], |
|||
default: 2000 |
|||
}, |
|||
// 设置数值后是否自动开始滚动 |
|||
autoplay: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 要显示的小数位数 |
|||
decimals: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 是否在即将到达目标数值的时候,使用缓慢滚动的效果 |
|||
useEasing: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 十进制分割 |
|||
decimal: { |
|||
type: [Number, String], |
|||
default: "." |
|||
}, |
|||
// 字体颜色 |
|||
color: { |
|||
type: String, |
|||
default: "#303133" |
|||
}, |
|||
// 字体大小 |
|||
fontSize: { |
|||
type: [Number, String], |
|||
default: 50 |
|||
}, |
|||
// 是否加粗字体 |
|||
bold: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 千位分隔符,类似金额的分割(¥23,321.05中的",") |
|||
separator: { |
|||
type: String, |
|||
default: "" |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
localStartVal: this.startVal, |
|||
displayValue: this.formatNumber(this.startVal), |
|||
printVal: null, |
|||
paused: false, // 是否暂停 |
|||
localDuration: Number(this.duration), |
|||
startTime: null, // 开始的时间 |
|||
timestamp: null, // 时间戳 |
|||
remaining: null, // 停留的时间 |
|||
rAF: null, |
|||
lastTime: 0 // 上一次的时间 |
|||
}; |
|||
}, |
|||
computed: { |
|||
countDown() { |
|||
return this.startVal > this.endVal; |
|||
}, |
|||
displayValueCom() { |
|||
let str; |
|||
let { displayValue, nullVal } = this; |
|||
if (isNaN(displayValue)) { |
|||
str = nullVal; |
|||
} else { |
|||
str = displayValue; |
|||
} |
|||
return str; |
|||
} |
|||
}, |
|||
watch: { |
|||
startVal() { |
|||
this.autoplay && this.start(); |
|||
}, |
|||
endVal() { |
|||
this.autoplay && this.start(); |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.autoplay && this.start(); |
|||
}, |
|||
methods: { |
|||
easingFn(t, b, c, d) { |
|||
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b; |
|||
}, |
|||
requestAnimationFrame(callback) { |
|||
const currTime = new Date().getTime(); |
|||
// 为了使setTimteout的尽可能的接近每秒60帧的效果 |
|||
const timeToCall = Math.max(0, 16 - (currTime - this.lastTime)); |
|||
const id = setTimeout(() => { |
|||
callback(currTime + timeToCall); |
|||
}, timeToCall); |
|||
this.lastTime = currTime + timeToCall; |
|||
return id; |
|||
}, |
|||
|
|||
cancelAnimationFrame(id) { |
|||
clearTimeout(id); |
|||
}, |
|||
// 开始滚动数字 |
|||
start() { |
|||
this.localStartVal = this.startVal; |
|||
this.startTime = null; |
|||
this.localDuration = this.duration; |
|||
this.paused = false; |
|||
this.rAF = this.requestAnimationFrame(this.count); |
|||
}, |
|||
// 暂定状态,重新再开始滚动;或者滚动状态下,暂停 |
|||
reStart() { |
|||
if (this.paused) { |
|||
this.resume(); |
|||
this.paused = false; |
|||
} else { |
|||
this.stop(); |
|||
this.paused = true; |
|||
} |
|||
}, |
|||
// 暂停 |
|||
stop() { |
|||
this.cancelAnimationFrame(this.rAF); |
|||
}, |
|||
// 重新开始(暂停的情况下) |
|||
resume() { |
|||
this.startTime = null; |
|||
this.localDuration = this.remaining; |
|||
this.localStartVal = this.printVal; |
|||
this.requestAnimationFrame(this.count); |
|||
}, |
|||
// 重置 |
|||
reset() { |
|||
this.startTime = null; |
|||
this.cancelAnimationFrame(this.rAF); |
|||
this.displayValue = this.formatNumber(this.startVal); |
|||
}, |
|||
count(timestamp) { |
|||
if (!this.startTime) this.startTime = timestamp; |
|||
this.timestamp = timestamp; |
|||
const progress = timestamp - this.startTime; |
|||
this.remaining = this.localDuration - progress; |
|||
if (this.useEasing) { |
|||
if (this.countDown) { |
|||
this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration); |
|||
} else { |
|||
this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration); |
|||
} |
|||
} else { |
|||
if (this.countDown) { |
|||
this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration); |
|||
} else { |
|||
this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration); |
|||
} |
|||
} |
|||
if (this.countDown) { |
|||
this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal; |
|||
} else { |
|||
this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal; |
|||
} |
|||
this.displayValue = this.formatNumber(this.printVal); |
|||
if (progress < this.localDuration) { |
|||
this.rAF = this.requestAnimationFrame(this.count); |
|||
} else { |
|||
this.$emit("end"); |
|||
} |
|||
}, |
|||
// 判断是否数字 |
|||
isNumber(val) { |
|||
return !isNaN(parseFloat(val)); |
|||
}, |
|||
formatNumber(num) { |
|||
// 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错 |
|||
num = Number(num); |
|||
num = num.toFixed(Number(this.decimals)); |
|||
num += ""; |
|||
const x = num.split("."); |
|||
let x1 = x[0]; |
|||
const x2 = x.length > 1 ? this.decimal + x[1] : ""; |
|||
const rgx = /(\d+)(\d{3})/; |
|||
if (this.separator && !this.isNumber(this.separator)) { |
|||
while (rgx.test(x1)) { |
|||
x1 = x1.replace(rgx, "$1" + this.separator + "$2"); |
|||
} |
|||
} |
|||
return x1 + x2; |
|||
}, |
|||
// #ifndef VUE3 |
|||
destroyed() { |
|||
this.cancelAnimationFrame(this.rAF); |
|||
}, |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
unmounted() { |
|||
this.cancelAnimationFrame(this.rAF); |
|||
} |
|||
// #endif |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-count-num { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
text-align: center; |
|||
} |
|||
</style> |
@ -0,0 +1,153 @@ |
|||
<template> |
|||
<view class="u-divider" :style="{ |
|||
height: height == 'auto' ? 'auto' : height + 'rpx', |
|||
backgroundColor: bgColor, |
|||
marginBottom: marginBottom + 'rpx', |
|||
marginTop: marginTop + 'rpx' |
|||
}" @tap="click"> |
|||
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view> |
|||
<view v-if="useSlot" class="u-divider-text" :style="{ |
|||
color: color, |
|||
fontSize: fontSize + 'rpx' |
|||
}"><slot /></view> |
|||
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* divider 分割线 |
|||
* @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。 |
|||
* @tutorial https://www.uviewui.com/components/divider.html |
|||
* @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx |
|||
* @property {String} border-color 线条颜色,优先级高于type(默认#dcdfe6) |
|||
* @property {String} color 文字颜色(默认#909399) |
|||
* @property {String Number} fontSize 字体大小,单位rpx(默认26) |
|||
* @property {String} bg-color 整个divider的背景颜色(默认呢#ffffff) |
|||
* @property {String Number} height 整个divider的高度,单位rpx(默认40) |
|||
* @property {String} type 将线条设置主题色(默认primary) |
|||
* @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true) |
|||
* @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0) |
|||
* @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0) |
|||
* @event {Function} click divider组件被点击时触发 |
|||
* @example <u-divider color="#fa3534">长河落日圆</u-divider> |
|||
*/ |
|||
export default { |
|||
name: 'u-divider', |
|||
props: { |
|||
// 单一边divider横线的宽度(数值),单位rpx。或者百分比 |
|||
halfWidth: { |
|||
type: [Number, String], |
|||
default: 150 |
|||
}, |
|||
// divider横线的颜色,如设置, |
|||
borderColor: { |
|||
type: String, |
|||
default: '#dcdfe6' |
|||
}, |
|||
// 主题色,可以是primary|info|success|warning|error之一值 |
|||
type: { |
|||
type: String, |
|||
default: 'primary' |
|||
}, |
|||
// 文字颜色 |
|||
color: { |
|||
type: String, |
|||
default: '#909399' |
|||
}, |
|||
// 文字大小,单位rpx |
|||
fontSize: { |
|||
type: [Number, String], |
|||
default: 26 |
|||
}, |
|||
// 整个divider的背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '#ffffff' |
|||
}, |
|||
// 整个divider的高度单位rpx |
|||
height: { |
|||
type: [Number, String], |
|||
default: 'auto' |
|||
}, |
|||
// 上边距 |
|||
marginTop: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 下边距 |
|||
marginBottom: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙 |
|||
useSlot: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
computed: { |
|||
lineStyle() { |
|||
let style = {}; |
|||
if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth; |
|||
else style.width = this.halfWidth + 'rpx'; |
|||
// borderColor优先级高于type值 |
|||
if(this.borderColor) style.borderColor = this.borderColor; |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
click() { |
|||
this.$emit('click'); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
.u-divider { |
|||
width: 100%; |
|||
position: relative; |
|||
text-align: center; |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
overflow: hidden; |
|||
flex-direction: row; |
|||
} |
|||
|
|||
.u-divider-line { |
|||
border-bottom: 1px solid $u-border-color; |
|||
transform: scale(1, 0.5); |
|||
transform-origin: center; |
|||
|
|||
&--bordercolor--primary { |
|||
border-color: $u-type-primary; |
|||
} |
|||
|
|||
&--bordercolor--success { |
|||
border-color: $u-type-success; |
|||
} |
|||
|
|||
&--bordercolor--error { |
|||
border-color: $u-type-primary; |
|||
} |
|||
|
|||
&--bordercolor--info { |
|||
border-color: $u-type-info; |
|||
} |
|||
|
|||
&--bordercolor--warning { |
|||
border-color: $u-type-warning; |
|||
} |
|||
} |
|||
|
|||
.u-divider-text { |
|||
white-space: nowrap; |
|||
padding: 0 16rpx; |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
} |
|||
</style> |
@ -0,0 +1,147 @@ |
|||
<template> |
|||
<view class="u-dropdown-item" v-if="active" @touchmove.stop.prevent="() => {}" @tap.stop.prevent="() => {}"> |
|||
<block v-if="!$slots.default && !$slots.$default"> |
|||
<scroll-view scroll-y="true" :style="{ |
|||
height: $u.addUnit(height) |
|||
}"> |
|||
<view class="u-dropdown-item__options"> |
|||
<u-cell-group> |
|||
<u-cell-item @click="cellClick(item.value)" :arrow="false" :title="item.label" v-for="(item, index) in options" |
|||
:key="index" :title-style="{ |
|||
color: value == item.value ? activeColor : inactiveColor |
|||
}"> |
|||
<u-icon v-if="getValue() == item.value" name="checkbox-mark" :color="activeColor" size="32"></u-icon> |
|||
</u-cell-item> |
|||
</u-cell-group> |
|||
</view> |
|||
</scroll-view> |
|||
</block> |
|||
<slot v-else /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* dropdown-item 下拉菜单 |
|||
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景 |
|||
* @tutorial http://uviewui.com/components/dropdown.html |
|||
* @property {String | Number} v-model 双向绑定选项卡选择值 |
|||
* @property {String} title 菜单项标题 |
|||
* @property {Array[Object]} options 选项数据,如果传入了默认slot,此参数无效 |
|||
* @property {Boolean} disabled 是否禁用此选项卡(默认false) |
|||
* @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300) |
|||
* @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto) |
|||
* @example <u-dropdown-item title="标题"></u-dropdown-item> |
|||
*/ |
|||
export default { |
|||
name: 'u-dropdown-item', |
|||
emits: ["update:modelValue", "input", "change"], |
|||
props: { |
|||
// 当前选中项的value值 |
|||
value: { |
|||
type: [Number, String, Array], |
|||
default: '' |
|||
}, |
|||
modelValue: { |
|||
type: [Number, String, Array], |
|||
default: '' |
|||
}, |
|||
// 菜单项标题 |
|||
title: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 选项数据,如果传入了默认slot,此参数无效 |
|||
options: { |
|||
type: Array, |
|||
default () { |
|||
return [] |
|||
} |
|||
}, |
|||
// 是否禁用此菜单项 |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 下拉弹窗的高度 |
|||
height: { |
|||
type: [Number, String], |
|||
default: 'auto' |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
active: false, // 当前项是否处于展开状态 |
|||
activeColor: '#2979ff', // 激活时左边文字和右边对勾图标的颜色 |
|||
inactiveColor: '#606266', // 未激活时左边文字和右边对勾图标的颜色 |
|||
} |
|||
}, |
|||
computed: { |
|||
// 监听props是否发生了变化,有些值需要传递给父组件u-dropdown,无法双向绑定 |
|||
propsChange() { |
|||
return `${this.title}-${this.disabled}`; |
|||
} |
|||
}, |
|||
watch: { |
|||
propsChange(n) { |
|||
// 当值变化时,通知父组件重新初始化,让父组件执行每个子组件的init()方法 |
|||
// 将所有子组件数据重新整理一遍 |
|||
if (this.parent) this.parent.init(); |
|||
} |
|||
}, |
|||
created() { |
|||
// 父组件的实例 |
|||
this.parent = false; |
|||
}, |
|||
methods: { |
|||
getValue(){ |
|||
// #ifndef VUE3 |
|||
return this.value; |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
return this.modelValue; |
|||
// #endif |
|||
}, |
|||
init() { |
|||
// 获取父组件u-dropdown |
|||
let parent = this.$u.$parent.call(this, 'u-dropdown'); |
|||
if (parent) { |
|||
this.parent = parent; |
|||
// 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色 |
|||
this.activeColor = parent.activeColor; |
|||
this.inactiveColor = parent.inactiveColor; |
|||
// 将本组件的this,放入到父组件的children数组中,让父组件可以操作本(子)组件的方法和属性 |
|||
// push进去前,显判断是否已经存在了本实例,因为在子组件内部数据变化时,会通过父组件重新初始化子组件 |
|||
let exist = parent.children.find(val => { |
|||
return this === val; |
|||
}) |
|||
if (!exist) parent.children.push(this); |
|||
if (parent.children.length == 1) this.active = true; |
|||
// 父组件无法监听children的变化,故将子组件的title,传入父组件的menuList数组中 |
|||
parent.menuList.push({ |
|||
title: this.title, |
|||
disabled: this.disabled |
|||
}); |
|||
} |
|||
}, |
|||
// cell被点击 |
|||
cellClick(value) { |
|||
// 修改通过v-model绑定的值 |
|||
this.$emit('input', value); |
|||
this.$emit("update:modelValue", value); |
|||
// 通知父组件(u-dropdown)收起菜单 |
|||
this.parent.close(); |
|||
// 发出事件,抛出当前勾选项的value |
|||
this.$emit('change', value); |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.init(); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
</style> |
@ -0,0 +1,299 @@ |
|||
<template> |
|||
<view class="u-dropdown"> |
|||
<view class="u-dropdown__menu" :style="{ |
|||
height: $u.addUnit(height) |
|||
}" :class="{ |
|||
'u-border-bottom': borderBottom |
|||
}"> |
|||
<view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)"> |
|||
<view class="u-flex"> |
|||
<text class="u-dropdown__menu__item__text" :style="{ |
|||
color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor, |
|||
fontSize: $u.addUnit(titleSize) |
|||
}">{{item.title}}</text> |
|||
<view class="u-dropdown__menu__item__arrow" :class="{ |
|||
'u-dropdown__menu__item__arrow--rotate': index === current |
|||
}"> |
|||
<u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="u-dropdown__content" :style="[contentStyle, { |
|||
transition: `opacity ${duration / 1000}s linear`, |
|||
top: $u.addUnit(height), |
|||
height: contentHeight + 'px' |
|||
}]" |
|||
@tap="maskClick" @touchmove.stop.prevent> |
|||
<view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]"> |
|||
<slot></slot> |
|||
</view> |
|||
<view class="u-dropdown__content__mask"></view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* dropdown 下拉菜单 |
|||
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景 |
|||
* @tutorial http://uviewui.com/components/dropdown.html |
|||
* @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff) |
|||
* @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266) |
|||
* @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true) |
|||
* @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true) |
|||
* @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300) |
|||
* @property {String | Number} height 标题菜单的高度,单位任意(默认80) |
|||
* @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0) |
|||
* @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false) |
|||
* @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28) |
|||
* @event {Function} open 下拉菜单被打开时触发 |
|||
* @event {Function} close 下拉菜单被关闭时触发 |
|||
* @example <u-dropdown></u-dropdown> |
|||
*/ |
|||
export default { |
|||
name: 'u-dropdown', |
|||
emits: ["open", "close"], |
|||
props: { |
|||
// 菜单标题和选项的激活态颜色 |
|||
activeColor: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
}, |
|||
// 菜单标题和选项的未激活态颜色 |
|||
inactiveColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// 点击遮罩是否关闭菜单 |
|||
closeOnClickMask: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 点击当前激活项标题是否关闭菜单 |
|||
closeOnClickSelf: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 过渡时间 |
|||
duration: { |
|||
type: [Number, String], |
|||
default: 300 |
|||
}, |
|||
// 标题菜单的高度,单位任意,数值默认为rpx单位 |
|||
height: { |
|||
type: [Number, String], |
|||
default: 80 |
|||
}, |
|||
// 是否显示下边框 |
|||
borderBottom: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 标题的字体大小 |
|||
titleSize: { |
|||
type: [Number, String], |
|||
default: 28 |
|||
}, |
|||
// 下拉出来的内容部分的圆角值 |
|||
borderRadius: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 菜单右侧的icon图标 |
|||
menuIcon: { |
|||
type: String, |
|||
default: 'arrow-down' |
|||
}, |
|||
// 菜单右侧图标的大小 |
|||
menuIconSize: { |
|||
type: [Number, String], |
|||
default: 26 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
showDropdown: true, // 是否打开下来菜单, |
|||
menuList: [], // 显示的菜单 |
|||
active: false, // 下拉菜单的状态 |
|||
// 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0, |
|||
// 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新 |
|||
current: 99999, |
|||
// 外层内容的样式,初始时处于底层,且透明 |
|||
contentStyle: { |
|||
zIndex: -1, |
|||
opacity: 0 |
|||
}, |
|||
// 让某个菜单保持高亮的状态 |
|||
highlightIndex: 99999, |
|||
contentHeight: 0 |
|||
} |
|||
}, |
|||
computed: { |
|||
// 下拉出来部分的样式 |
|||
popupStyle() { |
|||
let style = {}; |
|||
// 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏 |
|||
style.transform = `translateY(${this.active ? 0 : '-100%'})` |
|||
style['transition-duration'] = this.duration / 1000 + 's'; |
|||
style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`; |
|||
return style; |
|||
} |
|||
}, |
|||
created() { |
|||
// 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错 |
|||
this.children = []; |
|||
}, |
|||
mounted() { |
|||
this.getContentHeight(); |
|||
}, |
|||
methods: { |
|||
init() { |
|||
// 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍 |
|||
// 以保证数据的正确性 |
|||
this.menuList = []; |
|||
this.children.map(child => { |
|||
child.init(); |
|||
}) |
|||
}, |
|||
// 点击菜单 |
|||
menuClick(index) { |
|||
// 判断是否被禁用 |
|||
if (this.menuList[index].disabled) return; |
|||
// 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单 |
|||
if (index === this.current && this.closeOnClickSelf) { |
|||
this.close(); |
|||
// 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了 |
|||
setTimeout(() => { |
|||
this.children[index].active = false; |
|||
}, this.duration) |
|||
return; |
|||
} |
|||
this.open(index); |
|||
}, |
|||
// 打开下拉菜单 |
|||
open(index) { |
|||
// 重置高亮索引,否则会造成多个菜单同时高亮 |
|||
// this.highlightIndex = 9999; |
|||
// 展开时,设置下拉内容的样式 |
|||
this.contentStyle = { |
|||
zIndex: 11, |
|||
} |
|||
// 标记展开状态以及当前展开项的索引 |
|||
this.active = true; |
|||
this.current = index; |
|||
// 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的 |
|||
// 之所以不是因display: none,是因为nvue没有display这个属性 |
|||
this.children.map((val, idx) => { |
|||
val.active = index == idx ? true : false; |
|||
}) |
|||
this.$emit('open', this.current); |
|||
}, |
|||
// 设置下拉菜单处于收起状态 |
|||
close() { |
|||
this.$emit('close', this.current); |
|||
// 设置为收起状态,同时current归位,设置为空字符串 |
|||
this.active = false; |
|||
this.current = 99999; |
|||
// 下拉内容的样式进行调整,不透明度设置为0 |
|||
this.contentStyle = { |
|||
zIndex: -1, |
|||
opacity: 0 |
|||
} |
|||
}, |
|||
// 点击遮罩 |
|||
maskClick() { |
|||
// 如果不允许点击遮罩,直接返回 |
|||
if (!this.closeOnClickMask) return; |
|||
this.close(); |
|||
}, |
|||
// 外部手动设置某个菜单高亮 |
|||
highlight(index = undefined) { |
|||
this.highlightIndex = index !== undefined ? index : 99999; |
|||
}, |
|||
// 获取下拉菜单内容的高度 |
|||
getContentHeight() { |
|||
// 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度 |
|||
// 才能让遮罩占满菜单一下,直到屏幕底部的高度 |
|||
// this.$u.sys()为uView封装的获取设备信息的方法 |
|||
let windowHeight = this.$u.sys().windowHeight; |
|||
this.$uGetRect('.u-dropdown__menu').then(res => { |
|||
// 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本) |
|||
// H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离 |
|||
// 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成 |
|||
// 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动 |
|||
this.contentHeight = windowHeight - res.bottom; |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-dropdown { |
|||
flex: 1; |
|||
width: 100%; |
|||
position: relative; |
|||
|
|||
&__menu { |
|||
@include vue-flex; |
|||
position: relative; |
|||
z-index: 11; |
|||
height: 80rpx; |
|||
|
|||
&__item { |
|||
flex: 1; |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
|
|||
&__text { |
|||
font-size: 28rpx; |
|||
color: $u-content-color; |
|||
} |
|||
|
|||
&__arrow { |
|||
margin-left: 6rpx; |
|||
transition: transform .3s; |
|||
align-items: center; |
|||
@include vue-flex; |
|||
|
|||
&--rotate { |
|||
transform: rotate(180deg); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
&__content { |
|||
position: absolute; |
|||
z-index: 8; |
|||
width: 100%; |
|||
left: 0px; |
|||
bottom: 0; |
|||
overflow: hidden; |
|||
|
|||
|
|||
&__mask { |
|||
position: absolute; |
|||
z-index: 9; |
|||
background: rgba(0, 0, 0, .3); |
|||
width: 100%; |
|||
left: 0; |
|||
top: 0; |
|||
bottom: 0; |
|||
} |
|||
|
|||
&__popup { |
|||
position: relative; |
|||
z-index: 10; |
|||
transition: all 0.3s; |
|||
transform: translate3D(0, -100%, 0); |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
|
|||
} |
|||
</style> |
@ -0,0 +1,193 @@ |
|||
<template> |
|||
<view class="u-empty" v-if="show" :style="{ |
|||
marginTop: marginTop + 'rpx' |
|||
}"> |
|||
<u-icon |
|||
:name="src ? src : 'empty-' + mode" |
|||
:custom-style="iconStyle" |
|||
:label="text ? text : icons[mode]" |
|||
label-pos="bottom" |
|||
:label-color="color" |
|||
:label-size="fontSize" |
|||
:size="iconSize" |
|||
:color="iconColor" |
|||
margin-top="14" |
|||
></u-icon> |
|||
<view class="u-slot-wrap"> |
|||
<slot name="bottom"></slot> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* empty 内容为空 |
|||
* @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。 |
|||
* @tutorial https://www.uviewui.com/components/empty.html |
|||
* @property {String} color 文字颜色(默认#c0c4cc) |
|||
* @property {String} text 文字提示(默认“无内容”) |
|||
* @property {String} src 自定义图标路径,如定义,mode参数会失效 |
|||
* @property {String Number} font-size 提示文字的大小,单位rpx(默认28) |
|||
* @property {String} mode 内置的图标,见官网说明(默认data) |
|||
* @property {String Number} img-width 图标的宽度,单位rpx(默认240) |
|||
* @property {String} img-height 图标的高度,单位rpx(默认auto) |
|||
* @property {String Number} margin-top 组件距离上一个元素之间的距离(默认0) |
|||
* @property {Boolean} show 是否显示组件(默认true) |
|||
* @event {Function} click 点击组件时触发 |
|||
* @event {Function} close 点击关闭按钮时触发 |
|||
* @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty> |
|||
*/ |
|||
export default { |
|||
name: "u-empty", |
|||
props: { |
|||
// 图标路径 |
|||
src: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 提示文字 |
|||
text: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 文字颜色 |
|||
color: { |
|||
type: String, |
|||
default: '#c0c4cc' |
|||
}, |
|||
// 图标的颜色 |
|||
iconColor: { |
|||
type: String, |
|||
default: '#c0c4cc' |
|||
}, |
|||
// 图标的大小 |
|||
iconSize: { |
|||
type: [String, Number], |
|||
default: 120 |
|||
}, |
|||
// 文字大小,单位rpx |
|||
fontSize: { |
|||
type: [String, Number], |
|||
default: 26 |
|||
}, |
|||
// 选择预置的图标类型 |
|||
mode: { |
|||
type: String, |
|||
default: 'data' |
|||
}, |
|||
// 图标宽度,单位rpx |
|||
imgWidth: { |
|||
type: [String, Number], |
|||
default: 120 |
|||
}, |
|||
// 图标高度,单位rpx |
|||
imgHeight: { |
|||
type: [String, Number], |
|||
default: 'auto' |
|||
}, |
|||
// 是否显示组件 |
|||
show: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 组件距离上一个元素之间的距离 |
|||
marginTop: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
iconStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
icons: { |
|||
car: '购物车为空', |
|||
page: '页面不存在', |
|||
search: '没有搜索结果', |
|||
address: '没有收货地址', |
|||
wifi: '没有WiFi', |
|||
order: '订单为空', |
|||
coupon: '没有优惠券', |
|||
favor: '暂无收藏', |
|||
permission: '无权限', |
|||
history: '无历史记录', |
|||
news: '无新闻列表', |
|||
message: '消息列表为空', |
|||
list: '列表为空', |
|||
data: '数据为空' |
|||
}, |
|||
// icons: [{ |
|||
// icon: 'car', |
|||
// text: '购物车为空' |
|||
// },{ |
|||
// icon: 'page', |
|||
// text: '页面不存在' |
|||
// },{ |
|||
// icon: 'search', |
|||
// text: '没有搜索结果' |
|||
// },{ |
|||
// icon: 'address', |
|||
// text: '没有收货地址' |
|||
// },{ |
|||
// icon: 'wifi', |
|||
// text: '没有WiFi' |
|||
// },{ |
|||
// icon: 'order', |
|||
// text: '订单为空' |
|||
// },{ |
|||
// icon: 'coupon', |
|||
// text: '没有优惠券' |
|||
// },{ |
|||
// icon: 'favor', |
|||
// text: '暂无收藏' |
|||
// },{ |
|||
// icon: 'permission', |
|||
// text: '无权限' |
|||
// },{ |
|||
// icon: 'history', |
|||
// text: '无历史记录' |
|||
// },{ |
|||
// icon: 'news', |
|||
// text: '无新闻列表' |
|||
// },{ |
|||
// icon: 'message', |
|||
// text: '消息列表为空' |
|||
// },{ |
|||
// icon: 'list', |
|||
// text: '列表为空' |
|||
// },{ |
|||
// icon: 'data', |
|||
// text: '数据为空' |
|||
// }], |
|||
|
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-empty { |
|||
@include vue-flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 100%; |
|||
} |
|||
|
|||
.u-image { |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.u-slot-wrap { |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
margin-top: 20rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,397 @@ |
|||
<template> |
|||
<view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }"> |
|||
<view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]"> |
|||
<view class="u-label" :class="[required ? 'u-required' : '']" :style="{ |
|||
justifyContent: justifyContent, |
|||
flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1' |
|||
}"> |
|||
<view class="u-icon-wrap" v-if="icon"> |
|||
<u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon> |
|||
</view> |
|||
<slot name="icon"></slot> |
|||
<text class="u-label-text" :class="[$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text> |
|||
</view> |
|||
<view class="fild-body"> |
|||
<view class="u-flex-1 u-flex" :style="[inputWrapStyle]"> |
|||
<textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="getValue()" |
|||
:placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength" |
|||
:focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm" |
|||
@tap="fieldClick" /> |
|||
<input |
|||
v-else |
|||
:style="[fieldStyle]" |
|||
:type="type" |
|||
class="u-flex-1 u-field__input-wrap" |
|||
:value="getValue()" |
|||
:password="password || type === 'password'" |
|||
:placeholder="placeholder" |
|||
:placeholderStyle="placeholderStyle" |
|||
:disabled="disabled" |
|||
:maxlength="inputMaxlength" |
|||
:focus="focus" |
|||
:confirmType="confirmType" |
|||
@focus="onFocus" |
|||
@blur="onBlur" |
|||
@input="onInput" |
|||
@confirm="onConfirm" |
|||
@tap="fieldClick" |
|||
/> |
|||
</view> |
|||
<u-icon :size="clearSize" v-if="clearable && getValue() != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/> |
|||
<view class="u-button-wrap"><slot name="right" /></view> |
|||
<u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" /> |
|||
</view> |
|||
</view> |
|||
<view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{ |
|||
paddingLeft: labelWidth + 'rpx' |
|||
}">{{ errorMessage }}</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* field 输入框 |
|||
* @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。 |
|||
* @tutorial https://www.uviewui.com/components/field.html |
|||
* @property {String} type 输入框的类型(默认text) |
|||
* @property {String} icon label左边的图标,限uView的图标名称 |
|||
* @property {Object} icon-style 左边图标的样式,对象形式 |
|||
* @property {Boolean} right-icon 输入框右边的图标名称,限uView的图标名称(默认false) |
|||
* @property {Boolean} required 是否必填,左边您显示红色"*"号(默认false) |
|||
* @property {String} label 输入框左边的文字提示 |
|||
* @property {Boolean} password 是否密码输入方式(用点替换文字),type为text时有效(默认false) |
|||
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true) |
|||
* @property {Number String} label-width label的宽度,单位rpx(默认130) |
|||
* @property {String} label-align label的文字对齐方式(默认left) |
|||
* @property {Object} field-style 自定义输入框的样式,对象形式 |
|||
* @property {Number | String} clear-size 清除图标的大小,单位rpx(默认30) |
|||
* @property {String} input-align 输入框内容对齐方式(默认left) |
|||
* @property {Boolean} border-bottom 是否显示field的下边框(默认true) |
|||
* @property {Boolean} border-top 是否显示field的上边框(默认false) |
|||
* @property {String} icon-color 左边通过icon配置的图标的颜色(默认#606266) |
|||
* @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true) |
|||
* @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息 |
|||
* @property {String} placeholder 输入框的提示文字 |
|||
* @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd" |
|||
* @property {Boolean} focus 是否自动获得焦点(默认false) |
|||
* @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false) |
|||
* @property {Boolean} disabled 是否不可输入(默认false) |
|||
* @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140) |
|||
* @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done) |
|||
* @event {Function} input 输入框内容发生变化时触发 |
|||
* @event {Function} focus 输入框获得焦点时触发 |
|||
* @event {Function} blur 输入框失去焦点时触发 |
|||
* @event {Function} confirm 点击完成按钮时触发 |
|||
* @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发 |
|||
* @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发,这样设计是考虑到传递右边的图标,一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明 |
|||
* @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field> |
|||
*/ |
|||
export default { |
|||
name:"u-field", |
|||
emits: ["update:modelValue", "input", "focus", "blur", "confirm", "right-icon-click", "click"], |
|||
props: { |
|||
value: [Number, String], |
|||
modelValue: [Number, String], |
|||
icon: String, |
|||
rightIcon: String, |
|||
// arrowDirection: { |
|||
// type: String, |
|||
// default: 'right' |
|||
// }, |
|||
required: Boolean, |
|||
label: String, |
|||
password: Boolean, |
|||
clearable: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 左边标题的宽度单位rpx |
|||
labelWidth: { |
|||
type: [Number, String], |
|||
default: 130 |
|||
}, |
|||
// 对齐方式,left|center|right |
|||
labelAlign: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
inputAlign: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
iconColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
autoHeight: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
errorMessage: { |
|||
type: [String, Boolean], |
|||
default: '' |
|||
}, |
|||
placeholder: String, |
|||
placeholderStyle: String, |
|||
focus: Boolean, |
|||
fixed: Boolean, |
|||
type: { |
|||
type: String, |
|||
default: 'text' |
|||
}, |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
maxlength: { |
|||
type: [Number, String], |
|||
default: 140 |
|||
}, |
|||
confirmType: { |
|||
type: String, |
|||
default: 'done' |
|||
}, |
|||
// lable的位置,可选为 left-左边,top-上边 |
|||
labelPosition: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// 输入框的自定义样式 |
|||
fieldStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
// 清除按钮的大小 |
|||
clearSize: { |
|||
type: [Number, String], |
|||
default: 30 |
|||
}, |
|||
// lable左边的图标样式,对象形式 |
|||
iconStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
// 是否显示上边框 |
|||
borderTop: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否显示下边框 |
|||
borderBottom: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否自动去除两端的空格 |
|||
trim: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
focused: false, |
|||
itemIndex: 0, |
|||
}; |
|||
}, |
|||
computed: { |
|||
inputWrapStyle() { |
|||
let style = {}; |
|||
style.textAlign = this.inputAlign; |
|||
// 判断lable的位置,如果是left的话,让input左边两边有间隙 |
|||
if(this.labelPosition == 'left') { |
|||
style.margin = `0 8rpx`; |
|||
} else { |
|||
// 如果lable是top的,input的左边就没必要有间隙了 |
|||
style.marginRight = `8rpx`; |
|||
} |
|||
return style; |
|||
}, |
|||
rightIconStyle() { |
|||
let style = {}; |
|||
if (this.arrowDirection == 'top') style.transform = 'roate(-90deg)'; |
|||
if (this.arrowDirection == 'bottom') style.transform = 'roate(90deg)'; |
|||
else style.transform = 'roate(0deg)'; |
|||
return style; |
|||
}, |
|||
labelStyle() { |
|||
let style = {}; |
|||
if(this.labelAlign == 'left') style.justifyContent = 'flext-start'; |
|||
if(this.labelAlign == 'center') style.justifyContent = 'center'; |
|||
if(this.labelAlign == 'right') style.justifyContent = 'flext-end'; |
|||
return style; |
|||
}, |
|||
// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法 |
|||
justifyContent() { |
|||
if(this.labelAlign == 'left') return 'flex-start'; |
|||
if(this.labelAlign == 'center') return 'center'; |
|||
if(this.labelAlign == 'right') return 'flex-end'; |
|||
}, |
|||
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值 |
|||
inputMaxlength() { |
|||
return Number(this.maxlength) |
|||
}, |
|||
// label的位置 |
|||
fieldInnerStyle() { |
|||
let style = {}; |
|||
if(this.labelPosition == 'left') { |
|||
style.flexDirection = 'row'; |
|||
} else { |
|||
style.flexDirection = 'column'; |
|||
} |
|||
|
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
getValue(){ |
|||
// #ifndef VUE3 |
|||
return this.value; |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
return this.modelValue; |
|||
// #endif |
|||
}, |
|||
onInput(event) { |
|||
let value = event.detail.value; |
|||
// 判断是否去除空格 |
|||
if(this.trim) value = this.$u.trim(value); |
|||
this.$emit('input', value); |
|||
this.$emit("update:modelValue", value); |
|||
}, |
|||
onFocus(event) { |
|||
this.focused = true; |
|||
this.$emit('focus', event); |
|||
}, |
|||
onBlur(event) { |
|||
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错 |
|||
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时 |
|||
setTimeout(() => { |
|||
this.focused = false; |
|||
}, 100) |
|||
this.$emit('blur', event); |
|||
}, |
|||
onConfirm(e) { |
|||
this.$emit('confirm', e.detail.value); |
|||
}, |
|||
onClear(event) { |
|||
this.$emit('input', ''); |
|||
this.$emit("update:modelValue", ''); |
|||
}, |
|||
rightIconClick() { |
|||
this.$emit('right-icon-click'); |
|||
this.$emit('click'); |
|||
}, |
|||
fieldClick() { |
|||
this.$emit('click'); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-field { |
|||
font-size: 28rpx; |
|||
padding: 20rpx 28rpx; |
|||
text-align: left; |
|||
position: relative; |
|||
color: $u-main-color; |
|||
} |
|||
|
|||
.u-field-inner { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-textarea-inner { |
|||
align-items: flex-start; |
|||
} |
|||
|
|||
.u-textarea-class { |
|||
min-height: 96rpx; |
|||
width: auto; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.fild-body { |
|||
@include vue-flex; |
|||
flex: 1; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-arror-right { |
|||
margin-left: 8rpx; |
|||
} |
|||
|
|||
.u-label-text { |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
} |
|||
|
|||
.u-label-left-gap { |
|||
margin-left: 6rpx; |
|||
} |
|||
|
|||
.u-label-postion-top { |
|||
flex-direction: column; |
|||
align-items: flex-start; |
|||
} |
|||
|
|||
.u-label { |
|||
width: 130rpx; |
|||
flex: 1 1 130rpx; |
|||
text-align: left; |
|||
position: relative; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-required::before { |
|||
content: '*'; |
|||
position: absolute; |
|||
left: -16rpx; |
|||
font-size: 14px; |
|||
color: $u-type-error; |
|||
height: 9px; |
|||
line-height: 1; |
|||
} |
|||
|
|||
.u-field__input-wrap { |
|||
position: relative; |
|||
overflow: hidden; |
|||
font-size: 28rpx; |
|||
height: 48rpx; |
|||
flex: 1; |
|||
width: auto; |
|||
} |
|||
|
|||
.u-clear-icon { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.u-error-message { |
|||
color: $u-type-error; |
|||
font-size: 26rpx; |
|||
text-align: left; |
|||
} |
|||
|
|||
.placeholder-style { |
|||
color: rgb(150, 151, 153); |
|||
} |
|||
|
|||
.u-input-class { |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.u-button-wrap { |
|||
margin-left: 8rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,448 @@ |
|||
<template> |
|||
<view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}"> |
|||
<view class="u-form-item__body" :style="{ |
|||
flexDirection: elLabelPosition == 'left' ? 'row' : 'column' |
|||
}"> |
|||
<!-- 微信小程序中,将一个参数设置空字符串,结果会变成字符串"true" --> |
|||
<view class="u-form-item--left" :style="{ |
|||
width: uLabelWidth, |
|||
flex: `0 0 ${uLabelWidth}`, |
|||
marginBottom: elLabelPosition == 'left' ? 0 : '10rpx', |
|||
}"> |
|||
<!-- 为了块对齐 --> |
|||
<view class="u-form-item--left__content" v-if="required || leftIcon || label"> |
|||
<!-- nvue不支持伪元素before --> |
|||
<text v-if="required" class="u-form-item--left__content--required">*</text> |
|||
<view class="u-form-item--left__content__icon" v-if="leftIcon"> |
|||
<u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon> |
|||
</view> |
|||
<view class="u-form-item--left__content__label" :style="[elLabelStyle, { |
|||
'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end' |
|||
}]"> |
|||
{{label}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="u-form-item--right u-flex"> |
|||
<view class="u-form-item--right__content"> |
|||
<view class="u-form-item--right__content__slot "> |
|||
<slot /> |
|||
</view> |
|||
<view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon"> |
|||
<u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon> |
|||
<slot name="right" /> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{ |
|||
paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0', |
|||
}">{{validateMessage}}</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import Emitter from '../../libs/util/emitter.js'; |
|||
import schema from '../../libs/util/async-validator'; |
|||
// 去除警告信息 |
|||
schema.warning = function() {}; |
|||
|
|||
/** |
|||
* form-item 表单item |
|||
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。 |
|||
* @tutorial http://uviewui.com/components/form.html |
|||
* @property {String} label 左侧提示文字 |
|||
* @property {Object} prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的 |
|||
* @property {Boolean} border-bottom 是否显示表单域的下划线边框 |
|||
* @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方 |
|||
* @property {String Number} label-width 提示文字的宽度,单位rpx(默认90) |
|||
* @property {Object} label-style lable的样式,对象形式 |
|||
* @property {String} label-align lable的对齐方式 |
|||
* @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址 |
|||
* @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址 |
|||
* @property {Object} left-icon-style 左侧图标的样式,对象形式 |
|||
* @property {Object} right-icon-style 右侧图标的样式,对象形式 |
|||
* @property {Boolean} required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false) |
|||
* @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item> |
|||
*/ |
|||
|
|||
export default { |
|||
name: 'u-form-item', |
|||
mixins: [Emitter], |
|||
inject: { |
|||
uForm: { |
|||
default () { |
|||
return null |
|||
} |
|||
} |
|||
}, |
|||
props: { |
|||
// input的label提示语 |
|||
label: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 绑定的值 |
|||
prop: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 是否显示表单域的下划线边框 |
|||
borderBottom: { |
|||
type: [String, Boolean], |
|||
default: '' |
|||
}, |
|||
// label的位置,left-左边,top-上边 |
|||
labelPosition: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// label的宽度,单位rpx |
|||
labelWidth: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// lable的样式,对象形式 |
|||
labelStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// lable字体的对齐方式 |
|||
labelAlign: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 右侧图标 |
|||
rightIcon: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 左侧图标 |
|||
leftIcon: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 左侧图标的样式 |
|||
leftIconStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 左侧图标的样式 |
|||
rightIconStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
}, |
|||
// 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置 |
|||
required: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
initialValue: '', // 存储的默认值 |
|||
// isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成 |
|||
validateState: '', // 是否校验成功 |
|||
validateMessage: '', // 校验失败的提示语 |
|||
// 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色, |
|||
errorType: ['message'], |
|||
fieldValue: '', // 获取当前子组件input的输入的值 |
|||
// 父组件的参数,在computed计算中,无法得知this.parent发生变化,故将父组件的参数值,放到data中 |
|||
parentData: { |
|||
borderBottom: true, |
|||
labelWidth: 90, |
|||
labelPosition: 'left', |
|||
labelStyle: {}, |
|||
labelAlign: 'left', |
|||
} |
|||
}; |
|||
}, |
|||
watch: { |
|||
validateState(val) { |
|||
this.broadcastInputError(); |
|||
}, |
|||
// 监听u-form组件的errorType的变化 |
|||
"uForm.errorType"(val) { |
|||
this.errorType = val; |
|||
this.broadcastInputError(); |
|||
}, |
|||
}, |
|||
computed: { |
|||
// 计算后的label宽度,由于需要多个判断,故放到computed中 |
|||
uLabelWidth() { |
|||
// 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto |
|||
return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this |
|||
.elLabelWidth)) : '100%'; |
|||
}, |
|||
showError() { |
|||
return type => { |
|||
// 如果errorType数组中含有none,或者toast提示类型 |
|||
if (this.errorType.indexOf('none') >= 0) return false; |
|||
else if (this.errorType.indexOf(type) >= 0) return true; |
|||
else return false; |
|||
} |
|||
}, |
|||
// label的宽度 |
|||
elLabelWidth() { |
|||
// label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值 |
|||
return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData |
|||
.labelWidth : |
|||
90); |
|||
}, |
|||
// label的样式 |
|||
elLabelStyle() { |
|||
return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle : |
|||
{}); |
|||
}, |
|||
// label的位置,左侧或者上方 |
|||
elLabelPosition() { |
|||
return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition : |
|||
'left'); |
|||
}, |
|||
// label的对齐方式 |
|||
elLabelAlign() { |
|||
return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left'); |
|||
}, |
|||
// label的下划线 |
|||
elBorderBottom() { |
|||
// 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值 |
|||
return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom : |
|||
true; |
|||
} |
|||
}, |
|||
methods: { |
|||
broadcastInputError() { |
|||
// 子组件发出事件,第三个参数为true或者false,true代表有错误 |
|||
this.broadcast('u-input', 'onFormItemError', this.validateState === 'error' && this.showError('border')); |
|||
}, |
|||
// 判断是否需要required校验 |
|||
setRules() { |
|||
let that = this; |
|||
// 由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成 |
|||
// 从父组件u-form拿到当前u-form-item需要验证 的规则 |
|||
// let rules = this.getRules(); |
|||
// if (rules.length) { |
|||
// this.isRequired = rules.some(rule => { |
|||
// // 如果有必填项,就返回,没有的话,就是undefined |
|||
// return rule.required; |
|||
// }); |
|||
// } |
|||
// #ifndef VUE3 |
|||
// blur事件 |
|||
this.$on('onFieldBlur', that.onFieldBlur); |
|||
// change事件 |
|||
this.$on('onFieldChange', that.onFieldChange); |
|||
// #endif |
|||
// #ifdef VUE3 |
|||
|
|||
// #endif |
|||
}, |
|||
|
|||
// 从u-form的rules属性中,取出当前u-form-item的校验规则 |
|||
getRules() { |
|||
// 父组件的所有规则 |
|||
let rules = this.parent.rules; |
|||
rules = rules ? rules[this.prop] : []; |
|||
// 保证返回的是一个数组形式 |
|||
return [].concat(rules || []); |
|||
}, |
|||
|
|||
// blur事件时进行表单校验 |
|||
onFieldBlur() { |
|||
this.validation('blur'); |
|||
}, |
|||
|
|||
// change事件进行表单校验 |
|||
onFieldChange() { |
|||
this.validation('change'); |
|||
}, |
|||
|
|||
// 过滤出符合要求的rule规则 |
|||
getFilteredRule(triggerType = '') { |
|||
let rules = this.getRules(); |
|||
// 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证 |
|||
if (!triggerType) return rules; |
|||
// 历遍判断规则是否有对应的事件,比如blur,change触发等的事件 |
|||
// 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change'] |
|||
// 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性 |
|||
return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1); |
|||
}, |
|||
|
|||
// 校验数据 |
|||
validation(trigger, callback = () => {}) { |
|||
// 检验之间,先获取需要校验的值 |
|||
this.fieldValue = this.parent.model[this.prop]; |
|||
// blur和change是否有当前方式的校验规则 |
|||
let rules = this.getFilteredRule(trigger); |
|||
// 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为 |
|||
// 对count变量的统计错误而无法进入上一层的回调 |
|||
if (!rules || rules.length === 0) { |
|||
return callback(''); |
|||
} |
|||
// 设置当前的装填,标识为校验中 |
|||
this.validateState = 'validating'; |
|||
// 调用async-validator的方法 |
|||
let validator = new schema({ |
|||
[this.prop]: rules |
|||
}); |
|||
validator.validate({ |
|||
[this.prop]: this.fieldValue |
|||
}, { |
|||
firstFields: true |
|||
}, (errors, fields) => { |
|||
// 记录状态和报错信息 |
|||
this.validateState = !errors ? 'success' : 'error'; |
|||
this.validateMessage = errors ? errors[0].message : ''; |
|||
// 调用回调方法 |
|||
callback(this.validateMessage); |
|||
}); |
|||
}, |
|||
|
|||
// 清空当前的u-form-item |
|||
resetField() { |
|||
this.parent.model[this.prop] = this.initialValue; |
|||
// 设置为`success`状态,只是为了清空错误标记 |
|||
this.validateState = 'success'; |
|||
} |
|||
}, |
|||
|
|||
// 组件创建完成时,将当前实例保存到u-form中 |
|||
mounted() { |
|||
// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用 |
|||
this.parent = this.$u.$parent.call(this, 'u-form'); |
|||
if (this.parent) { |
|||
// 历遍parentData中的属性,将parent中的同名属性赋值给parentData |
|||
Object.keys(this.parentData).map(key => { |
|||
this.parentData[key] = this.parent[key]; |
|||
}); |
|||
// 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验 |
|||
if (this.prop) { |
|||
// 将本实例添加到父组件中 |
|||
this.parent.fields.push(this); |
|||
this.errorType = this.parent.errorType; |
|||
// 设置初始值 |
|||
this.initialValue = this.fieldValue; |
|||
// 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的 |
|||
// 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空 |
|||
this.$nextTick(() => { |
|||
this.setRules(); |
|||
}) |
|||
} |
|||
} |
|||
}, |
|||
// #ifndef VUE3 |
|||
// 组件销毁前,将实例从u-form的缓存中移除 |
|||
beforeDestroy() { |
|||
// 如果当前没有prop的话表示当前不要进行删除(因为没有注入) |
|||
if (this.parent && this.prop) { |
|||
this.parent.fields.map((item, index) => { |
|||
if (item === this) this.parent.fields.splice(index, 1); |
|||
}) |
|||
} |
|||
}, |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
beforeUnmount() { |
|||
// 如果当前没有prop的话表示当前不要进行删除(因为没有注入) |
|||
if (this.parent && this.prop) { |
|||
this.parent.fields.map((item, index) => { |
|||
if (item === this) this.parent.fields.splice(index, 1); |
|||
}) |
|||
} |
|||
}, |
|||
// #endif |
|||
|
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-form-item { |
|||
@include vue-flex; |
|||
// align-items: flex-start; |
|||
padding: 20rpx 0; |
|||
font-size: 28rpx; |
|||
color: $u-main-color; |
|||
box-sizing: border-box; |
|||
line-height: $u-form-item-height; |
|||
flex-direction: column; |
|||
|
|||
&__border-bottom--error:after { |
|||
border-color: $u-type-error; |
|||
} |
|||
|
|||
&__body { |
|||
@include vue-flex; |
|||
} |
|||
|
|||
&--left { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
|
|||
&__content { |
|||
position: relative; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
padding-right: 10rpx; |
|||
flex: 1; |
|||
|
|||
&__icon { |
|||
margin-right: 8rpx; |
|||
} |
|||
|
|||
&--required { |
|||
position: absolute; |
|||
left: -16rpx; |
|||
vertical-align: middle; |
|||
color: $u-type-error; |
|||
padding-top: 6rpx; |
|||
} |
|||
|
|||
&__label { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&--right { |
|||
flex: 1; |
|||
|
|||
&__content { |
|||
@include vue-flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
|
|||
&__slot { |
|||
flex: 1; |
|||
/* #ifndef MP */ |
|||
@include vue-flex; |
|||
align-items: center; |
|||
/* #endif */ |
|||
} |
|||
|
|||
&__icon { |
|||
margin-left: 10rpx; |
|||
color: $u-light-color; |
|||
font-size: 30rpx; |
|||
} |
|||
} |
|||
} |
|||
|
|||
&__message { |
|||
font-size: 24rpx; |
|||
line-height: 24rpx; |
|||
color: $u-type-error; |
|||
margin-top: 12rpx; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,134 @@ |
|||
<template> |
|||
<view class="u-form"><slot /></view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* form 表单 |
|||
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。 |
|||
* @tutorial http://uviewui.com/components/form.html |
|||
* @property {Object} model 表单数据对象 |
|||
* @property {Boolean} border-bottom 是否显示表单域的下划线边框 |
|||
* @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方 |
|||
* @property {String Number} label-width 提示文字的宽度,单位rpx(默认90) |
|||
* @property {Object} label-style lable的样式,对象形式 |
|||
* @property {String} label-align lable的对齐方式 |
|||
* @property {Object} rules 通过ref设置,见官网说明 |
|||
* @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message']) |
|||
* @example <u-form :model="form" ref="uForm"></u-form> |
|||
*/ |
|||
|
|||
export default { |
|||
name: 'u-form', |
|||
props: { |
|||
// 当前form的需要验证字段的集合 |
|||
model: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 验证规则 |
|||
// rules: { |
|||
// type: [Object, Function, Array], |
|||
// default() { |
|||
// return {}; |
|||
// } |
|||
// }, |
|||
// 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色, |
|||
// border-bottom-下边框呈现红色,none-无提示 |
|||
errorType: { |
|||
type: Array, |
|||
default() { |
|||
return ['message', 'toast'] |
|||
} |
|||
}, |
|||
// 是否显示表单域的下划线边框 |
|||
borderBottom: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// label的位置,left-左边,top-上边 |
|||
labelPosition: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// label的宽度,单位rpx |
|||
labelWidth: { |
|||
type: [String, Number], |
|||
default: 90 |
|||
}, |
|||
// lable字体的对齐方式 |
|||
labelAlign: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// lable的样式,对象形式 |
|||
labelStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
}, |
|||
provide() { |
|||
return { |
|||
uForm: this |
|||
}; |
|||
}, |
|||
data() { |
|||
return { |
|||
rules: {} |
|||
}; |
|||
}, |
|||
created() { |
|||
// 存储当前form下的所有u-form-item的实例 |
|||
// 不能定义在data中,否则微信小程序会造成循环引用而报错 |
|||
this.fields = []; |
|||
}, |
|||
methods: { |
|||
setRules(rules) { |
|||
this.rules = rules; |
|||
}, |
|||
// 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法 |
|||
resetFields() { |
|||
this.fields.map(field => { |
|||
field.resetField(); |
|||
}); |
|||
}, |
|||
// 校验全部数据 |
|||
validate(callback) { |
|||
return new Promise(resolve => { |
|||
// 对所有的u-form-item进行校验 |
|||
let valid = true; // 默认通过 |
|||
let count = 0; // 用于标记是否检查完毕 |
|||
let errorArr = []; // 存放错误信息 |
|||
this.fields.map(field => { |
|||
// 调用每一个u-form-item实例的validation的校验方法 |
|||
field.validation('', error => { |
|||
// 如果任意一个u-form-item校验不通过,就意味着整个表单不通过 |
|||
if (error) { |
|||
valid = false; |
|||
errorArr.push(error); |
|||
} |
|||
// 当历遍了所有的u-form-item时,调用promise的then方法 |
|||
if (++count === this.fields.length) { |
|||
resolve(valid); // 进入promise的then方法 |
|||
// 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息 |
|||
if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) { |
|||
this.$u.toast(errorArr[0]); |
|||
} |
|||
// 调用回调方法 |
|||
if (typeof callback == 'function') callback(valid); |
|||
} |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
</style> |
@ -0,0 +1,52 @@ |
|||
<template> |
|||
<u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm"> |
|||
<view class="u-update-content"> |
|||
<rich-text :nodes="content"></rich-text> |
|||
</view> |
|||
</u-modal> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
show: false, |
|||
content: ` |
|||
1. 修复badge组件的size参数无效问题<br> |
|||
2. 新增Modal模态框组件<br> |
|||
3. 新增压窗屏组件,可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br> |
|||
4. 修复键盘组件在微信小程序上遮罩无效的问题 |
|||
`, |
|||
} |
|||
}, |
|||
onReady() { |
|||
this.show = true; |
|||
}, |
|||
methods: { |
|||
cancel() { |
|||
this.closeModal(); |
|||
}, |
|||
confirm() { |
|||
this.closeModal(); |
|||
}, |
|||
closeModal() { |
|||
uni.navigateBack(); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-full-content { |
|||
background-color: #00C777; |
|||
} |
|||
|
|||
.u-update-content { |
|||
font-size: 26rpx; |
|||
color: $u-content-color; |
|||
line-height: 1.7; |
|||
padding: 30rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,54 @@ |
|||
<template> |
|||
<view class="u-gap" :style="[gapStyle]"></view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* gap 间隔槽 |
|||
* @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量 |
|||
* @tutorial https://www.uviewui.com/components/gap.html |
|||
* @property {String} bg-color 背景颜色(默认#f3f4f6) |
|||
* @property {String Number} height 分割槽高度,单位rpx(默认30) |
|||
* @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0) |
|||
* @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0) |
|||
* @example <u-gap height="80" bg-color="#bbb"></u-gap> |
|||
*/ |
|||
export default { |
|||
name: "u-gap", |
|||
props: { |
|||
bgColor: { |
|||
type: String, |
|||
default: 'transparent ' // 背景透明 |
|||
}, |
|||
// 高度 |
|||
height: { |
|||
type: [String, Number], |
|||
default: 30 |
|||
}, |
|||
// 与上一个组件的距离 |
|||
marginTop: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 与下一个组件的距离 |
|||
marginBottom: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
}, |
|||
computed: { |
|||
gapStyle() { |
|||
return { |
|||
backgroundColor: this.bgColor, |
|||
height: this.height + 'rpx', |
|||
marginTop: this.marginTop + 'rpx', |
|||
marginBottom: this.marginBottom + 'rpx' |
|||
}; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
</style> |
@ -0,0 +1,127 @@ |
|||
<template> |
|||
<view class="u-grid-item" :hover-class="parentData.hoverClass" |
|||
:hover-stay-time="200" @tap="click" :style="{ |
|||
background: bgColor, |
|||
width: width, |
|||
}"> |
|||
<view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']"> |
|||
<slot /> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* gridItem 提示 |
|||
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用 |
|||
* @tutorial https://www.uviewui.com/components/grid.html |
|||
* @property {String} bg-color 宫格的背景颜色(默认#ffffff) |
|||
* @property {String Number} index 点击宫格时,返回的值 |
|||
* @property {Object} custom-style 自定义样式,对象形式 |
|||
* @event {Function} click 点击宫格触发 |
|||
* @example <u-grid-item></u-grid-item> |
|||
*/ |
|||
export default { |
|||
name: "u-grid-item", |
|||
emits: ["click"], |
|||
props: { |
|||
// 背景颜色 |
|||
bgColor: { |
|||
type: String, |
|||
default: '#ffffff' |
|||
}, |
|||
// 点击时返回的index |
|||
index: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 自定义样式,对象形式 |
|||
customStyle: { |
|||
type: Object, |
|||
default() { |
|||
return { |
|||
padding: '30rpx 0' |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
parentData: { |
|||
hoverClass: '', // 按下去的时候,是否显示背景灰色 |
|||
col: 3, // 父组件划分的宫格数 |
|||
border: true, // 是否显示边框,根据父组件决定 |
|||
} |
|||
}; |
|||
}, |
|||
created() { |
|||
// 父组件的实例 |
|||
this.updateParentData(); |
|||
// this.parent在updateParentData()中定义 |
|||
this.parent.children.push(this); |
|||
}, |
|||
computed: { |
|||
// 每个grid-item的宽度 |
|||
width() { |
|||
return 100 / Number(this.parentData.col) + '%'; |
|||
}, |
|||
}, |
|||
methods: { |
|||
// 获取父组件的参数 |
|||
updateParentData() { |
|||
// 此方法写在mixin中 |
|||
this.getParentData('u-grid'); |
|||
}, |
|||
click() { |
|||
this.$emit('click', this.index); |
|||
this.parent && this.parent.click(this.index); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-grid-item { |
|||
box-sizing: border-box; |
|||
background: #fff; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
position: relative; |
|||
flex-direction: column; |
|||
|
|||
/* #ifdef MP */ |
|||
position: relative; |
|||
float: left; |
|||
/* #endif */ |
|||
} |
|||
|
|||
.u-grid-item-hover { |
|||
background: #f7f7f7 !important; |
|||
} |
|||
|
|||
.u-grid-marker-box { |
|||
position: absolute; |
|||
/* #ifndef APP-NVUE */ |
|||
display: inline-flex; |
|||
/* #endif */ |
|||
line-height: 0; |
|||
} |
|||
|
|||
.u-grid-marker-wrap { |
|||
position: absolute; |
|||
} |
|||
|
|||
.u-grid-item-box { |
|||
padding: 30rpx 0; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-direction: column; |
|||
flex: 1; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,109 @@ |
|||
<template> |
|||
<view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* grid 宫格布局 |
|||
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。 |
|||
* @tutorial https://www.uviewui.com/components/grid.html |
|||
* @property {String Number} col 宫格的列数(默认3) |
|||
* @property {Boolean} border 是否显示宫格的边框(默认true) |
|||
* @property {Boolean} hover-class 点击宫格的时候,是否显示按下的灰色背景(默认false) |
|||
* @event {Function} click 点击宫格触发 |
|||
* @example <u-grid :col="3" @click="click"></u-grid> |
|||
*/ |
|||
export default { |
|||
name: 'u-grid', |
|||
emits: ["click"], |
|||
props: { |
|||
// 分成几列 |
|||
col: { |
|||
type: [Number, String], |
|||
default: 3 |
|||
}, |
|||
// 是否显示边框 |
|||
border: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右 |
|||
align: { |
|||
type: String, |
|||
default: 'left' |
|||
}, |
|||
// 宫格按压时的样式类,"none"为无效果 |
|||
hoverClass: { |
|||
type: String, |
|||
default: 'u-hover-class' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
index: 0, |
|||
} |
|||
}, |
|||
watch: { |
|||
// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件 |
|||
parentData() { |
|||
if(this.children.length) { |
|||
this.children.map(child => { |
|||
// 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值) |
|||
typeof(child.updateParentData) == 'function' && child.updateParentData(); |
|||
}) |
|||
} |
|||
}, |
|||
}, |
|||
created() { |
|||
// 如果将children定义在data中,在微信小程序会造成循环引用而报错 |
|||
this.children = []; |
|||
}, |
|||
computed: { |
|||
// 计算父组件的值是否发生变化 |
|||
parentData() { |
|||
return [this.hoverClass, this.col, this.size, this.border]; |
|||
}, |
|||
// 宫格对齐方式 |
|||
gridStyle() { |
|||
let style = {}; |
|||
switch(this.align) { |
|||
case 'left': |
|||
style.justifyContent = 'flex-start'; |
|||
break; |
|||
case 'center': |
|||
style.justifyContent = 'center'; |
|||
break; |
|||
case 'right': |
|||
style.justifyContent = 'flex-end'; |
|||
break; |
|||
default: style.justifyContent = 'flex-start'; |
|||
}; |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
click(index) { |
|||
this.$emit('click', index); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-grid { |
|||
width: 100%; |
|||
/* #ifdef MP */ |
|||
position: relative; |
|||
box-sizing: border-box; |
|||
overflow: hidden; |
|||
/* #endif */ |
|||
|
|||
/* #ifndef MP */ |
|||
@include vue-flex; |
|||
flex-wrap: wrap; |
|||
align-items: center; |
|||
/* #endif */ |
|||
} |
|||
</style> |
@ -0,0 +1,341 @@ |
|||
<template> |
|||
<view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]"> |
|||
<image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image> |
|||
<view v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass" |
|||
@touchstart="touchstart"> |
|||
<text v-if="showDecimalIcon" :style="[decimalIconStyle]" :class="decimalIconClass" :hover-class="hoverClass" class="u-icon__decimal"></text> |
|||
</view> |
|||
<!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示,微信小程序不传值默认为null,故需要增加null的判断 --> |
|||
<text v-if="label !== '' && label !== null" class="u-icon__label" :style="{ |
|||
color: labelColor, |
|||
fontSize: $u.addUnit(labelSize), |
|||
marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0, |
|||
marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0, |
|||
marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0, |
|||
marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0, |
|||
}">{{ label }} |
|||
</text> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* icon 图标 |
|||
* @description 基于字体的图标集,包含了大多数常见场景的图标。 |
|||
* @tutorial https://www.uviewui.com/components/icon.html |
|||
* @property {String} name 图标名称,见示例图标集 |
|||
* @property {String} color 图标颜色(默认inherit) |
|||
* @property {String | Number} size 图标字体大小,单位rpx(默认32) |
|||
* @property {String | Number} label-size label字体大小,单位rpx(默认28) |
|||
* @property {String} label 图标右侧的label文字(默认28) |
|||
* @property {String} label-pos label文字相对于图标的位置,只能right或bottom(默认right) |
|||
* @property {String} label-color label字体颜色(默认#606266) |
|||
* @property {Object} custom-style icon的样式,对象形式 |
|||
* @property {String} custom-prefix 自定义字体图标库时,需要写上此值 |
|||
* @property {String | Number} margin-left label在右侧时与图标的距离,单位rpx(默认6) |
|||
* @property {String | Number} margin-top label在下方时与图标的距离,单位rpx(默认6) |
|||
* @property {String | Number} margin-bottom label在上方时与图标的距离,单位rpx(默认6) |
|||
* @property {String | Number} margin-right label在左侧时与图标的距离,单位rpx(默认6) |
|||
* @property {String} label-pos label相对于图标的位置,只能right或bottom(默认right) |
|||
* @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出 |
|||
* @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网 |
|||
* @property {String} width 显示图片小图标时的宽度 |
|||
* @property {String} height 显示图片小图标时的高度 |
|||
* @property {String} top 图标在垂直方向上的定位 |
|||
* @property {String} top 图标在垂直方向上的定位 |
|||
* @property {String} top 图标在垂直方向上的定位 |
|||
* @property {Boolean} show-decimal-icon 是否为DecimalIcon |
|||
* @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效 |
|||
* @property {String | Number} percent 显示的百分比,仅Decimal时有效 |
|||
* @event {Function} click 点击图标时触发 |
|||
* @example <u-icon name="photo" color="#2979ff" size="28"></u-icon> |
|||
*/ |
|||
export default { |
|||
name: 'u-icon', |
|||
emits: ["click", "touchstart"], |
|||
props: { |
|||
// 图标类名 |
|||
name: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 图标颜色,可接受主题色 |
|||
color: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 字体大小,单位rpx |
|||
size: { |
|||
type: [Number, String], |
|||
default: 'inherit' |
|||
}, |
|||
// 是否显示粗体 |
|||
bold: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 点击图标的时候传递事件出去的index(用于区分点击了哪一个) |
|||
index: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
// 触摸图标时的类名 |
|||
hoverClass: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 自定义扩展前缀,方便用户扩展自己的图标库 |
|||
customPrefix: { |
|||
type: String, |
|||
default: 'uicon' |
|||
}, |
|||
// 图标右边或者下面的文字 |
|||
label: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// label的位置,只能右边或者下边 |
|||
labelPos: { |
|||
type: String, |
|||
default: 'right' |
|||
}, |
|||
// label的大小 |
|||
labelSize: { |
|||
type: [String, Number], |
|||
default: '28' |
|||
}, |
|||
// label的颜色 |
|||
labelColor: { |
|||
type: String, |
|||
default: '#606266' |
|||
}, |
|||
// label与图标的距离(横向排列) |
|||
marginLeft: { |
|||
type: [String, Number], |
|||
default: '6' |
|||
}, |
|||
// label与图标的距离(竖向排列) |
|||
marginTop: { |
|||
type: [String, Number], |
|||
default: '6' |
|||
}, |
|||
// label与图标的距离(竖向排列) |
|||
marginRight: { |
|||
type: [String, Number], |
|||
default: '6' |
|||
}, |
|||
// label与图标的距离(竖向排列) |
|||
marginBottom: { |
|||
type: [String, Number], |
|||
default: '6' |
|||
}, |
|||
// 图片的mode |
|||
imgMode: { |
|||
type: String, |
|||
default: 'widthFix' |
|||
}, |
|||
// 自定义样式 |
|||
customStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {} |
|||
} |
|||
}, |
|||
// 用于显示图片小图标时,图片的宽度 |
|||
width: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 用于显示图片小图标时,图片的高度 |
|||
height: { |
|||
type: [String, Number], |
|||
default: '' |
|||
}, |
|||
// 用于解决某些情况下,让图标垂直居中的用途 |
|||
top: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 是否为DecimalIcon |
|||
showDecimalIcon: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 背景颜色,可接受主题色,仅Decimal时有效 |
|||
inactiveColor: { |
|||
type: String, |
|||
default: '#ececec' |
|||
}, |
|||
// 显示的百分比,仅Decimal时有效 |
|||
percent: { |
|||
type: [Number, String], |
|||
default: '50' |
|||
} |
|||
}, |
|||
computed: { |
|||
customClass() { |
|||
let classes = [] |
|||
let { customPrefix } = this; |
|||
if(this.name.indexOf("vk-icon-") == 0){ |
|||
customPrefix = "vk-icon"; |
|||
classes.push(this.name) |
|||
}else{ |
|||
classes.push(customPrefix + '-' + this.name) |
|||
} |
|||
// uView的自定义图标类名为u-iconfont |
|||
if (customPrefix == 'uicon') { |
|||
classes.push('u-iconfont') |
|||
} else { |
|||
classes.push(customPrefix) |
|||
} |
|||
// 主题色,通过类配置 |
|||
if (this.showDecimalIcon && this.inactiveColor && this.$u.config.type.includes(this.inactiveColor)) { |
|||
classes.push('u-icon__icon--' + this.inactiveColor) |
|||
} else if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color) |
|||
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别 |
|||
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名 |
|||
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU |
|||
classes = classes.join(' ') |
|||
//#endif |
|||
return classes |
|||
}, |
|||
iconStyle() { |
|||
let style = {} |
|||
style = { |
|||
fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size), |
|||
fontWeight: this.bold ? 'bold' : 'normal', |
|||
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中 |
|||
top: this.$u.addUnit(this.top) |
|||
} |
|||
// 非主题色值时,才当作颜色值 |
|||
if (this.showDecimalIcon && this.inactiveColor && !this.$u.config.type.includes(this.inactiveColor)) { |
|||
style.color = this.inactiveColor |
|||
} else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color |
|||
|
|||
return style |
|||
}, |
|||
// 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式 |
|||
isImg() { |
|||
return this.name.indexOf('/') !== -1 |
|||
}, |
|||
imgStyle() { |
|||
let style = {} |
|||
// 如果设置width和height属性,则优先使用,否则使用size属性 |
|||
style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size) |
|||
style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size) |
|||
return style |
|||
}, |
|||
decimalIconStyle() { |
|||
let style = {} |
|||
style = { |
|||
fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size), |
|||
fontWeight: this.bold ? 'bold' : 'normal', |
|||
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中 |
|||
top: this.$u.addUnit(this.top), |
|||
width: this.percent + '%' |
|||
} |
|||
// 非主题色值时,才当作颜色值 |
|||
if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color |
|||
return style |
|||
}, |
|||
decimalIconClass() { |
|||
let classes = [] |
|||
classes.push(this.customPrefix + '-' + this.name) |
|||
// uView的自定义图标类名为u-iconfont |
|||
if (this.customPrefix == 'uicon') { |
|||
classes.push('u-iconfont') |
|||
} else { |
|||
classes.push(this.customPrefix) |
|||
} |
|||
// 主题色,通过类配置 |
|||
if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color) |
|||
else classes.push('u-icon__icon--primary') |
|||
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别 |
|||
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名 |
|||
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU |
|||
classes = classes.join(' ') |
|||
//#endif |
|||
return classes |
|||
} |
|||
}, |
|||
methods: { |
|||
click() { |
|||
this.$emit('click', this.index) |
|||
}, |
|||
touchstart() { |
|||
this.$emit('touchstart', this.index) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import "../../libs/css/style.components.scss"; |
|||
@import '../../iconfont.css'; |
|||
|
|||
.u-icon { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
|
|||
&--left { |
|||
flex-direction: row-reverse; |
|||
align-items: center; |
|||
} |
|||
|
|||
&--right { |
|||
flex-direction: row; |
|||
align-items: center; |
|||
} |
|||
|
|||
&--top { |
|||
flex-direction: column-reverse; |
|||
justify-content: center; |
|||
} |
|||
|
|||
&--bottom { |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
} |
|||
|
|||
&__icon { |
|||
position: relative; |
|||
|
|||
&--primary { |
|||
color: $u-type-primary; |
|||
} |
|||
|
|||
&--success { |
|||
color: $u-type-success; |
|||
} |
|||
|
|||
&--error { |
|||
color: $u-type-error; |
|||
} |
|||
|
|||
&--warning { |
|||
color: $u-type-warning; |
|||
} |
|||
|
|||
&--info { |
|||
color: $u-type-info; |
|||
} |
|||
} |
|||
|
|||
&__decimal { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
display: inline-block; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
&__img { |
|||
height: auto; |
|||
will-change: transform; |
|||
} |
|||
|
|||
&__label { |
|||
line-height: 1; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,267 @@ |
|||
<template> |
|||
<view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]"> |
|||
<image |
|||
v-if="!isError" |
|||
:src="src" |
|||
:mode="mode" |
|||
@error="onErrorHandler" |
|||
@load="onLoadHandler" |
|||
:lazy-load="lazyLoad" |
|||
class="u-image__image" |
|||
:style="{ |
|||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius) |
|||
}" |
|||
></image> |
|||
<view |
|||
v-if="showLoading && loading" |
|||
class="u-image__loading" |
|||
:style="{ |
|||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius), |
|||
backgroundColor: bgColor |
|||
}" |
|||
> |
|||
<slot v-if="$slots.loading" name="loading" /> |
|||
<u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon> |
|||
</view> |
|||
<view |
|||
v-if="showError && isError && !loading" |
|||
class="u-image__error" |
|||
:style="{ |
|||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius) |
|||
}" |
|||
> |
|||
<slot v-if="$slots.error" name="error" /> |
|||
<u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* Image 图片 |
|||
* @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。 |
|||
* @tutorial https://uviewui.com/components/image.html |
|||
* @property {String} src 图片地址 |
|||
* @property {String} mode 裁剪模式,见官网说明 |
|||
* @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%) |
|||
* @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto) |
|||
* @property {String} shape 图片形状,circle-圆形,square-方形(默认square) |
|||
* @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0) |
|||
* @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true) |
|||
* @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false) |
|||
* @property {String} loading-icon 加载中的图标,或者小图片(默认 photo) |
|||
* @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle) |
|||
* @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true) |
|||
* @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true) |
|||
* @property {Boolean} fade 是否需要淡入效果(默认 true) |
|||
* @property {String Number} width 传入图片路径时图片的宽度 |
|||
* @property {String Number} height 传入图片路径时图片的高度 |
|||
* @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false) |
|||
* @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500) |
|||
* @event {Function} click 点击图片时触发 |
|||
* @event {Function} error 图片加载失败时触发 |
|||
* @event {Function} load 图片加载成功时触发 |
|||
* @example <u-image width="100%" height="300rpx" :src="src"></u-image> |
|||
*/ |
|||
export default { |
|||
name: 'u-image', |
|||
emits: ["click", "error", "load"], |
|||
props: { |
|||
// 图片地址 |
|||
src: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// 裁剪模式 |
|||
mode: { |
|||
type: String, |
|||
default: 'aspectFill' |
|||
}, |
|||
// 宽度,单位任意 |
|||
width: { |
|||
type: [String, Number], |
|||
default: '100%' |
|||
}, |
|||
// 高度,单位任意 |
|||
height: { |
|||
type: [String, Number], |
|||
default: 'auto' |
|||
}, |
|||
// 图片形状,circle-圆形,square-方形 |
|||
shape: { |
|||
type: String, |
|||
default: 'square' |
|||
}, |
|||
// 圆角,单位任意 |
|||
borderRadius: { |
|||
type: [String, Number], |
|||
default: 0 |
|||
}, |
|||
// 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序 |
|||
lazyLoad: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 开启长按图片显示识别微信小程序码菜单 |
|||
showMenuByLongpress: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 加载中的图标,或者小图片 |
|||
loadingIcon: { |
|||
type: String, |
|||
default: 'photo' |
|||
}, |
|||
// 加载失败的图标,或者小图片 |
|||
errorIcon: { |
|||
type: String, |
|||
default: 'error-circle' |
|||
}, |
|||
// 是否显示加载中的图标或者自定义的slot |
|||
showLoading: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示加载错误的图标或者自定义的slot |
|||
showError: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否需要淡入效果 |
|||
fade: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 只支持网络资源,只对微信小程序有效 |
|||
webp: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 过渡时间,单位ms |
|||
duration: { |
|||
type: [String, Number], |
|||
default: 500 |
|||
}, |
|||
// 背景颜色,用于深色页面加载图片时,为了和背景色融合 |
|||
bgColor: { |
|||
type: String, |
|||
default: '#f3f4f6' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
// 图片是否加载错误,如果是,则显示错误占位图 |
|||
isError: false, |
|||
// 初始化组件时,默认为加载中状态 |
|||
loading: true, |
|||
// 不透明度,为了实现淡入淡出的效果 |
|||
opacity: 1, |
|||
// 过渡时间,因为props的值无法修改,故需要一个中间值 |
|||
durationTime: this.duration, |
|||
// 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景 |
|||
backgroundStyle: {} |
|||
}; |
|||
}, |
|||
watch: { |
|||
src: { |
|||
immediate: true, |
|||
handler (n) { |
|||
if(!n) { |
|||
// 如果传入null或者'',或者false,或者undefined,标记为错误状态 |
|||
this.isError = true; |
|||
this.loading = false; |
|||
} else { |
|||
this.isError = false; |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
wrapStyle() { |
|||
let style = {}; |
|||
// 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位 |
|||
style.width = this.$u.addUnit(this.width); |
|||
style.height = this.$u.addUnit(this.height); |
|||
// 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值 |
|||
style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius); |
|||
// 如果设置圆角,必须要有hidden,否则可能圆角无效 |
|||
style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible'; |
|||
if (this.fade) { |
|||
style.opacity = this.opacity; |
|||
style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`; |
|||
} |
|||
return style; |
|||
} |
|||
}, |
|||
methods: { |
|||
// 点击图片 |
|||
onClick() { |
|||
this.$emit('click'); |
|||
}, |
|||
// 图片加载失败 |
|||
onErrorHandler(err) { |
|||
this.loading = false; |
|||
this.isError = true; |
|||
this.$emit('error', err); |
|||
}, |
|||
// 图片加载完成,标记loading结束 |
|||
onLoadHandler() { |
|||
this.loading = false; |
|||
this.isError = false; |
|||
this.$emit('load'); |
|||
// 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色 |
|||
// 否则无需fade效果时,png图片依然能看到下方的背景色 |
|||
if (!this.fade) return this.removeBgColor(); |
|||
// 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果 |
|||
this.opacity = 0; |
|||
// 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色) |
|||
// 到图片展示的过程中的淡入效果 |
|||
this.durationTime = 0; |
|||
// 延时50ms,否则在浏览器H5,过渡效果无效 |
|||
setTimeout(() => { |
|||
this.durationTime = this.duration; |
|||
this.opacity = 1; |
|||
setTimeout(() => { |
|||
this.removeBgColor(); |
|||
}, this.durationTime); |
|||
}, 50); |
|||
}, |
|||
// 移除图片的背景色 |
|||
removeBgColor() { |
|||
// 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景 |
|||
this.backgroundStyle = { |
|||
backgroundColor: 'transparent' |
|||
}; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
@import '../../libs/css/style.components.scss'; |
|||
|
|||
.u-image { |
|||
position: relative; |
|||
transition: opacity 0.5s ease-in-out; |
|||
|
|||
&__image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
&__loading, |
|||
&__error { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
@include vue-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background-color: $u-bg-color; |
|||
color: $u-tips-color; |
|||
font-size: 46rpx; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,89 @@ |
|||
<template> |
|||
<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" --> |
|||
<view> |
|||
<view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]"> |
|||
<view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]"> |
|||
<slot v-if="useSlot" /> |
|||
<block v-else> |
|||
<text>{{ index }}</text> |
|||
</block> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* indexAnchor 索引列表锚点 |
|||
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用 |
|||
* @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props |
|||
* @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false) |
|||
* @property {String Number} index 索引字符,如果定义了use-slot,此参数自动失效 |
|||
* @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}" |
|||
* @event {Function} default 锚点位置显示内容,默认为索引字符 |
|||
* @example <u-index-anchor :index="item" /> |
|||
*/ |
|||
export default { |
|||
name: "u-index-anchor", |
|||
props: { |
|||
useSlot: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
index: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
customStyle: { |
|||
type: Object, |
|||
default () { |
|||
return {} |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
active: false, |
|||
wrapperStyle: {}, |
|||
anchorStyle: {} |
|||
} |
|||
}, |
|||
created() { |
|||
this.parent = false; |
|||
}, |
|||
mounted() { |
|||
this.parent = this.$u.$parent.call(this, 'u-index-list'); |
|||
if(this.parent) { |
|||
this.parent.children.push(this); |
|||
this.parent.updateData(); |
|||
} |
|||
}, |
|||
computed: { |
|||
customAnchorStyle() { |
|||
return Object.assign(this.anchorStyle, this.customStyle); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-index-anchor { |
|||
box-sizing: border-box; |
|||
padding: 14rpx 24rpx; |
|||
color: #606266; |
|||
width: 100%; |
|||
font-weight: 500; |
|||
font-size: 28rpx; |
|||
line-height: 1.2; |
|||
background-color: rgb(245, 245, 245); |
|||
} |
|||
|
|||
.u-index-anchor--active { |
|||
right: 0; |
|||
left: 0; |
|||
color: #2979ff; |
|||
background-color: #fff; |
|||
} |
|||
</style> |
@ -0,0 +1,315 @@ |
|||
<template> |
|||
<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" --> |
|||
<view> |
|||
<view class="u-index-bar"> |
|||
<slot /> |
|||
<view v-if="showSidebar" class="u-index-bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove" |
|||
@touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop"> |
|||
<view v-for="(item, index) in indexList" :key="index" class="u-index-bar__index" :style="{zIndex: zIndex + 1, color: activeAnchorIndex === index ? activeColor : ''}" |
|||
:data-index="index"> |
|||
{{ item }} |
|||
</view> |
|||
</view> |
|||
<view class="u-indexed-list-alert" v-if="touchmove && indexList[touchmoveIndex]" :style="{ |
|||
zIndex: alertZIndex |
|||
}"> |
|||
<text>{{indexList[touchmoveIndex]}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
var indexList = function() { |
|||
var indexList = []; |
|||
var charCodeOfA = 'A'.charCodeAt(0); |
|||
for (var i = 0; i < 26; i++) { |
|||
indexList.push(String.fromCharCode(charCodeOfA + i)); |
|||
} |
|||
return indexList; |
|||
}; |
|||
|
|||
/** |
|||
* indexList 索引列表 |
|||
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用 |
|||
* @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props |
|||
* @property {Number String} scroll-top 当前滚动高度,自定义组件无法获得滚动条事件,所以依赖接入方传入 |
|||
* @property {Array} index-list 索引字符列表,数组(默认A-Z) |
|||
* @property {Number String} z-index 锚点吸顶时的层级(默认965) |
|||
* @property {Boolean} sticky 是否开启锚点自动吸顶(默认true) |
|||
* @property {Number String} offset-top 锚点自动吸顶时与顶部的距离(默认0) |
|||
* @property {String} highlight-color 锚点和右边索引字符高亮颜色(默认#2979ff) |
|||
* @event {Function} select 选中右边索引字符时触发 |
|||
* @example <u-index-list :scrollTop="scrollTop"></u-index-list> |
|||
*/ |
|||
export default { |
|||
name: "u-index-list", |
|||
props: { |
|||
sticky: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
zIndex: { |
|||
type: [Number, String], |
|||
default: '' |
|||
}, |
|||
scrollTop: { |
|||
type: [Number, String], |
|||
default: 0, |
|||
}, |
|||
offsetTop: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
indexList: { |
|||
type: Array, |
|||
default () { |
|||
return indexList() |
|||
} |
|||
}, |
|||
activeColor: { |
|||
type: String, |
|||
default: '#2979ff' |
|||
} |
|||
}, |
|||
created() { |
|||
// #ifdef H5 |
|||
this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 44; |
|||
// #endif |
|||
// #ifndef H5 |
|||
this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 0; |
|||
// #endif |
|||
// 只能在created生命周期定义children,如果在data定义,会因为循环引用而报错 |
|||
this.children = []; |
|||
}, |
|||
data() { |
|||
return { |
|||
activeAnchorIndex: 0, |
|||
showSidebar: true, |
|||
// children: [], |
|||
touchmove: false, |
|||
touchmoveIndex: 0, |
|||
} |
|||
}, |
|||
watch: { |
|||
scrollTop() { |
|||
this.updateData() |
|||
} |
|||
}, |
|||
computed: { |
|||
// 弹出toast的z-index值 |
|||
alertZIndex() { |
|||
return this.$u.zIndex.toast; |
|||
} |
|||
}, |
|||
methods: { |
|||
updateData() { |
|||
this.timer && clearTimeout(this.timer); |
|||
this.timer = setTimeout(() => { |
|||
this.showSidebar = !!this.children.length; |
|||
this.setRect().then(() => { |
|||
this.onScroll(); |
|||
}); |
|||
}, 0); |
|||
}, |
|||
setRect() { |
|||
return Promise.all([ |
|||
this.setAnchorsRect(), |
|||
this.setListRect(), |
|||
this.setSiderbarRect() |
|||
]); |
|||
}, |
|||
setAnchorsRect() { |
|||
return Promise.all(this.children.map((anchor, index) => anchor |
|||
.$uGetRect('.u-index-anchor-wrapper') |
|||
.then((rect) => { |
|||
Object.assign(anchor, { |
|||
height: rect.height, |
|||
top: rect.top |
|||
}); |
|||
}))); |
|||
}, |
|||
setListRect() { |
|||
return this.$uGetRect('.u-index-bar').then((rect) => { |
|||
Object.assign(this, { |
|||
height: rect.height, |
|||
top: rect.top + this.scrollTop |
|||
}); |
|||
}); |
|||
}, |
|||
setSiderbarRect() { |
|||
return this.$uGetRect('.u-index-bar__sidebar').then(rect => { |
|||
this.sidebar = { |
|||
height: rect.height, |
|||
top: rect.top |
|||
}; |
|||
}); |
|||
}, |
|||
getActiveAnchorIndex() { |
|||
const { |
|||
children |
|||
} = this; |
|||
const { |
|||
sticky |
|||
} = this; |
|||
for (let i = this.children.length - 1; i >= 0; i--) { |
|||
const preAnchorHeight = i > 0 ? children[i - 1].height : 0; |
|||
const reachTop = sticky ? preAnchorHeight : 0; |
|||
if (reachTop >= children[i].top) { |
|||
return i; |
|||
} |
|||
} |
|||
return -1; |
|||
}, |
|||
onScroll() { |
|||
const { |
|||
children = [] |
|||
} = this; |
|||
if (!children.length) { |
|||
return; |
|||
} |
|||
const { |
|||
sticky, |
|||
stickyOffsetTop, |
|||
zIndex, |
|||
scrollTop, |
|||
activeColor |
|||
} = this; |
|||
const active = this.getActiveAnchorIndex(); |
|||
this.activeAnchorIndex = active; |
|||
if (sticky) { |
|||
let isActiveAnchorSticky = false; |
|||
if (active !== -1) { |
|||
isActiveAnchorSticky = |
|||
children[active].top <= 0; |
|||
} |
|||
children.forEach((item, index) => { |
|||
if (index === active) { |
|||
let wrapperStyle = ''; |
|||
let anchorStyle = { |
|||
color: `${activeColor}` |
|||
}; |
|||
if (isActiveAnchorSticky) { |
|||
wrapperStyle = { |
|||
height: `${children[index].height}px` |
|||
}; |
|||
anchorStyle = { |
|||
position: 'fixed', |
|||
top: `${stickyOffsetTop}px`, |
|||
zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`, |
|||
color: `${activeColor}` |
|||
}; |
|||
} |
|||
item.active = active; |
|||
item.wrapperStyle = wrapperStyle; |
|||
item.anchorStyle = anchorStyle; |
|||
} else if (index === active - 1) { |
|||
const currentAnchor = children[index]; |
|||
const currentOffsetTop = currentAnchor.top; |
|||
const targetOffsetTop = index === children.length - 1 ? |
|||
this.top : |
|||
children[index + 1].top; |
|||
const parentOffsetHeight = targetOffsetTop - currentOffsetTop; |
|||
const translateY = parentOffsetHeight - currentAnchor.height; |
|||
const anchorStyle = { |
|||
position: 'relative', |
|||
transform: `translate3d(0, ${translateY}px, 0)`, |
|||
zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`, |
|||
color: `${activeColor}` |
|||
}; |
|||
item.active = active; |
|||
item.anchorStyle = anchorStyle; |
|||
} else { |
|||
item.active = false; |
|||
item.anchorStyle = ''; |
|||
item.wrapperStyle = ''; |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
onTouchMove(event) { |
|||
this.touchmove = true; |
|||
const sidebarLength = this.children.length; |
|||
const touch = event.touches[0]; |
|||
const itemHeight = this.sidebar.height / sidebarLength; |
|||
let clientY = 0; |
|||
clientY = touch.clientY; |
|||
let index = Math.floor((clientY - this.sidebar.top) / itemHeight); |
|||
if (index < 0) { |
|||
index = 0; |
|||
} else if (index > sidebarLength - 1) { |
|||
index = sidebarLength - 1; |
|||
} |
|||
this.touchmoveIndex = index; |
|||
this.scrollToAnchor(index); |
|||
}, |
|||
onTouchStop() { |
|||
this.touchmove = false; |
|||
this.scrollToAnchorIndex = null; |
|||
}, |
|||
scrollToAnchor(index) { |
|||
if (this.scrollToAnchorIndex === index) { |
|||
return; |
|||
} |
|||
this.scrollToAnchorIndex = index; |
|||
const anchor = this.children.find((item) => item.index === this.indexList[index]); |
|||
if (anchor) { |
|||
this.$emit('select', anchor.index); |
|||
uni.pageScrollTo({ |
|||
duration: 0, |
|||
scrollTop: anchor.top + this.scrollTop |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-index-bar { |
|||
position: relative |
|||
} |
|||
|
|||
.u-index-bar__sidebar { |
|||
position: fixed; |
|||
top: 50%; |
|||
right: 0; |
|||
@include vue-flex; |
|||
flex-direction: column; |
|||
text-align: center; |
|||
transform: translateY(-50%); |
|||
user-select: none; |
|||
z-index: 99; |
|||
} |
|||
|
|||
.u-index-bar__index { |
|||
font-weight: 500; |
|||
padding: 8rpx 18rpx; |
|||
font-size: 22rpx; |
|||
line-height: 1 |
|||
} |
|||
|
|||
.u-indexed-list-alert { |
|||
position: fixed; |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
right: 90rpx; |
|||
top: 50%; |
|||
margin-top: -60rpx; |
|||
border-radius: 24rpx; |
|||
font-size: 50rpx; |
|||
color: #fff; |
|||
background-color: rgba(0, 0, 0, 0.65); |
|||
@include vue-flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 0; |
|||
z-index: 9999999; |
|||
} |
|||
|
|||
.u-indexed-list-alert text { |
|||
line-height: 50rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,434 @@ |
|||
<template> |
|||
<view |
|||
class="u-input" |
|||
:class="{ |
|||
'u-input--border': border, |
|||
'u-input--error': validateState |
|||
}" |
|||
:style="{ |
|||
padding: `0 ${border ? 20 : 0}rpx`, |
|||
borderColor: borderColor, |
|||
textAlign: inputAlign |
|||
}" |
|||
@tap.stop="inputClick" |
|||
> |
|||
<textarea |
|||
v-if="type == 'textarea'" |
|||
class="u-input__input u-input__textarea" |
|||
:style="[getStyle]" |
|||
:value="defaultValue" |
|||
:placeholder="placeholder" |
|||
:placeholderStyle="placeholderStyle" |
|||
:disabled="disabled" |
|||
:maxlength="inputMaxlength" |
|||
:fixed="fixed" |
|||
:focus="focus" |
|||
:autoHeight="autoHeight" |
|||
:selection-end="uSelectionEnd" |
|||
:selection-start="uSelectionStart" |
|||
:cursor-spacing="getCursorSpacing" |
|||
:show-confirm-bar="showConfirmbar" |
|||
@input="handleInput" |
|||
@blur="handleBlur" |
|||
@focus="onFocus" |
|||
@confirm="onConfirm" |
|||
/> |
|||
<input |
|||
v-else |
|||
class="u-input__input" |
|||
:type="type == 'password' ? 'text' : type" |
|||
:style="[getStyle]" |
|||
:value="defaultValue" |
|||
:password="type == 'password' && !showPassword" |
|||
:placeholder="placeholder" |
|||
:placeholderStyle="placeholderStyle" |
|||
:disabled="disabled || type === 'select'" |
|||
:maxlength="inputMaxlength" |
|||
:focus="focus" |
|||
:confirmType="confirmType" |
|||
:cursor-spacing="getCursorSpacing" |
|||
:selection-end="uSelectionEnd" |
|||
:selection-start="uSelectionStart" |
|||
:show-confirm-bar="showConfirmbar" |
|||
@focus="onFocus" |
|||
@blur="handleBlur" |
|||
@input="handleInput" |
|||
@confirm="onConfirm" |
|||
/> |
|||
<view class="u-input__right-icon u-flex"> |
|||
<view |
|||
class="u-input__right-icon__clear u-input__right-icon__item" |
|||
@tap="onClear" |
|||
v-if="clearable && value != '' && focused" |
|||
> |
|||
<u-icon size="32" name="close-circle-fill" color="#c0c4cc" /> |
|||
</view> |
|||
<view |
|||
class="u-input__right-icon__clear u-input__right-icon__item" |
|||
v-if="passwordIcon && type == 'password'" |
|||
> |
|||
<u-icon |
|||
size="32" |
|||
:name="!showPassword ? 'eye' : 'eye-fill'" |
|||
color="#c0c4cc" |
|||
@click="showPassword = !showPassword" |
|||
/> |
|||
</view> |
|||
<view |
|||
class="u-input__right-icon--select u-input__right-icon__item" |
|||
v-if="type == 'select'" |
|||
:class="{ |
|||
'u-input__right-icon--select--reverse': selectOpen |
|||
}" |
|||
> |
|||
<u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import Emitter from "../../libs/util/emitter.js"; |
|||
|
|||
/** |
|||
* input 输入框 |
|||
* @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。 |
|||
* @tutorial http://uviewui.com/components/input.html |
|||
* @property {String} type 模式选择,见官网说明 |
|||
* @property {Boolean} clearable 是否显示右侧的清除图标(默认true) |
|||
* @property {} v-model 用于双向绑定输入框的值 |
|||
* @property {String} input-align 输入框文字的对齐方式(默认left) |
|||
* @property {String} placeholder placeholder显示值(默认 '请输入内容') |
|||
* @property {Boolean} disabled 是否禁用输入框(默认false) |
|||
* @property {String Number} maxlength 输入框的最大可输入长度(默认140) |
|||
* @property {String Number} selection-start 光标起始位置,自动聚焦时有效,需与selection-end搭配使用(默认-1) |
|||
* @property {String Number} maxlength 光标结束位置,自动聚焦时有效,需与selection-start搭配使用(默认-1) |
|||
* @property {String Number} cursor-spacing 指定光标与键盘的距离,单位px(默认0) |
|||
* @property {String} placeholderStyle placeholder的样式,字符串形式,如"color: red;"(默认 "color: #c0c4cc;") |
|||
* @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type为text时生效(默认done) |
|||
* @property {Object} custom-style 自定义输入框的样式,对象形式 |
|||
* @property {Boolean} focus 是否自动获得焦点(默认false) |
|||
* @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false) |
|||
* @property {Boolean} password-icon type为password时,是否显示右侧的密码查看图标(默认true) |
|||
* @property {Boolean} border 是否显示边框(默认false) |
|||
* @property {String} border-color 输入框的边框颜色(默认#dcdfe6) |
|||
* @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true) |
|||
* @property {String Number} height 高度,单位rpx(text类型时为70,textarea时为100) |
|||
* @example <u-input v-model="value" :type="type" :border="border" /> |
|||
*/ |
|||
export default { |
|||
name: "u-input", |
|||
emits: ["update:modelValue", "input", "change", "blur", "focus", "click", "touchstart"], |
|||
mixins: [Emitter], |
|||
props: { |
|||
value: { |
|||
type: [String, Number], |
|||
default: "" |
|||
}, |
|||
modelValue: { |
|||
type: [String, Number], |
|||
default: "" |
|||
}, |
|||
// 输入框的类型,textarea,text,number |
|||
type: { |
|||
type: String, |
|||
default: "text" |
|||
}, |
|||
inputAlign: { |
|||
type: String, |
|||
default: "left" |
|||
}, |
|||
placeholder: { |
|||
type: String, |
|||
default: "请输入内容" |
|||
}, |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
maxlength: { |
|||
type: [Number, String], |
|||
default: 140 |
|||
}, |
|||
placeholderStyle: { |
|||
type: String, |
|||
default: "color: #c0c4cc;" |
|||
}, |
|||
confirmType: { |
|||
type: String, |
|||
default: "done" |
|||
}, |
|||
// 输入框的自定义样式 |
|||
customStyle: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
} |
|||
}, |
|||
// 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true |
|||
fixed: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 是否自动获得焦点 |
|||
focus: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 密码类型时,是否显示右侧的密码图标 |
|||
passwordIcon: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// input|textarea是否显示边框 |
|||
border: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 输入框的边框颜色 |
|||
borderColor: { |
|||
type: String, |
|||
default: "#dcdfe6" |
|||
}, |
|||
autoHeight: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// type=select时,旋转右侧的图标,标识当前处于打开还是关闭select的状态 |
|||
// open-打开,close-关闭 |
|||
selectOpen: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 高度,单位rpx |
|||
height: { |
|||
type: [Number, String], |
|||
default: "" |
|||
}, |
|||
// 是否可清空 |
|||
clearable: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 指定光标与键盘的距离,单位 px |
|||
cursorSpacing: { |
|||
type: [Number, String], |
|||
default: 0 |
|||
}, |
|||
// 光标起始位置,自动聚焦时有效,需与selection-end搭配使用 |
|||
selectionStart: { |
|||
type: [Number, String], |
|||
default: -1 |
|||
}, |
|||
// 光标结束位置,自动聚焦时有效,需与selection-start搭配使用 |
|||
selectionEnd: { |
|||
type: [Number, String], |
|||
default: -1 |
|||
}, |
|||
// 是否自动去除两端的空格 |
|||
trim: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否显示键盘上方带有”完成“按钮那一栏 |
|||
showConfirmbar: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
defaultValue: this.getValue(), |
|||
inputHeight: 70, // input的高度 |
|||
textareaHeight: 100, // textarea的高度 |
|||
validateState: false, // 当前input的验证状态,用于错误时,边框是否改为红色 |
|||
focused: false, // 当前是否处于获得焦点的状态 |
|||
showPassword: false, // 是否预览密码 |
|||
lastValue: "" // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input时间 |
|||
}; |
|||
}, |
|||
watch: { |
|||
value(nVal, oVal) { |
|||
this.defaultValue = nVal; |
|||
// 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件 |
|||
if (nVal != oVal && this.type == "select") |
|||
this.handleInput({ |
|||
detail: { |
|||
value: nVal |
|||
} |
|||
}); |
|||
}, |
|||
modelValue(nVal, oVal) { |
|||
this.defaultValue = nVal; |
|||
// 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件 |
|||
if (nVal != oVal && this.type == "select") |
|||
this.handleInput({ |
|||
detail: { |
|||
value: nVal |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
computed: { |
|||
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值 |
|||
inputMaxlength() { |
|||
return Number(this.maxlength); |
|||
}, |
|||
getStyle() { |
|||
let style = {}; |
|||
// 如果没有自定义高度,就根据type为input还是textare来分配一个默认的高度 |
|||
style.minHeight = this.height |
|||
? this.height + "rpx" |
|||
: this.type == "textarea" |
|||
? this.textareaHeight + "rpx" |
|||
: this.inputHeight + "rpx"; |
|||
style = Object.assign(style, this.customStyle); |
|||
return style; |
|||
}, |
|||
// |
|||
getCursorSpacing() { |
|||
return Number(this.cursorSpacing); |
|||
}, |
|||
// 光标起始位置 |
|||
uSelectionStart() { |
|||
return String(this.selectionStart); |
|||
}, |
|||
// 光标结束位置 |
|||
uSelectionEnd() { |
|||
return String(this.selectionEnd); |
|||
} |
|||
}, |
|||
created() { |
|||
// 监听u-form-item发出的错误事件,将输入框边框变红色 |
|||
// #ifndef VUE3 |
|||
this.$on("onFormItemError", this.onFormItemError); |
|||
// #endif |
|||
}, |
|||
methods: { |
|||
getValue() { |
|||
// #ifndef VUE3 |
|||
return this.value; |
|||
// #endif |
|||
|
|||
// #ifdef VUE3 |
|||
return this.modelValue; |
|||
// #endif |
|||
}, |
|||
/** |
|||
* change 事件 |
|||
* @param event |
|||
*/ |
|||
handleInput(event) { |
|||
let value = event.detail.value; |
|||
// 判断是否去除空格 |
|||
if (this.trim) value = this.$u.trim(value); |
|||
// vue 原生的方法 return 出去 |
|||
this.$emit("input", value); |
|||
this.$emit("update:modelValue", value); |
|||
// 当前model 赋值 |
|||
this.defaultValue = value; |
|||
// 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上 |
|||
// 尚未更新到u-form-item,导致获取的值为空,从而校验混论 |
|||
// 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱 |
|||
setTimeout(() => { |
|||
// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理 |
|||
// #ifdef MP-TOUTIAO |
|||
if (this.$u.trim(value) == this.lastValue) return; |
|||
this.lastValue = value; |
|||
// #endif |
|||
// 将当前的值发送到 u-form-item 进行校验 |
|||
this.dispatch("u-form-item", "onFieldChange", value); |
|||
}, 40); |
|||
}, |
|||
/** |
|||
* blur 事件 |
|||
* @param event |
|||
*/ |
|||
handleBlur(event) { |
|||
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错 |
|||
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时 |
|||
setTimeout(() => { |
|||
this.focused = false; |
|||
}, 100); |
|||
// vue 原生的方法 return 出去 |
|||
this.$emit("blur", event.detail.value); |
|||
setTimeout(() => { |
|||
// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理 |
|||
// #ifdef MP-TOUTIAO |
|||
if (this.$u.trim(value) == this.lastValue) return; |
|||
this.lastValue = value; |
|||
// #endif |
|||
// 将当前的值发送到 u-form-item 进行校验 |
|||
this.dispatch("u-form-item", "onFieldBlur", event.detail.value); |
|||
}, 40); |
|||
}, |
|||
onFormItemError(status) { |
|||
this.validateState = status; |
|||
}, |
|||
onFocus(event) { |
|||
this.focused = true; |
|||
this.$emit("focus"); |
|||
}, |
|||
onConfirm(e) { |
|||
this.$emit("confirm", e.detail.value); |
|||
}, |
|||
onClear(event) { |
|||
this.$emit("input", ""); |
|||
this.$emit("update:modelValue", ""); |
|||
}, |
|||
inputClick() { |
|||
this.$emit("click"); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../../libs/css/style.components.scss"; |
|||
|
|||
.u-input { |
|||
position: relative; |
|||
flex: 1; |
|||
@include vue-flex; |
|||
|
|||
&__input { |
|||
//height: $u-form-item-height; |
|||
font-size: 28rpx; |
|||
color: $u-main-color; |
|||
flex: 1; |
|||
} |
|||
|
|||
&__textarea { |
|||
width: auto; |
|||
font-size: 28rpx; |
|||
color: $u-main-color; |
|||
padding: 10rpx 0; |
|||
line-height: normal; |
|||
flex: 1; |
|||
} |
|||
|
|||
&--border { |
|||
border-radius: 6rpx; |
|||
border-radius: 4px; |
|||
border: 1px solid $u-form-item-border-color; |
|||
} |
|||
|
|||
&--error { |
|||
border-color: $u-type-error !important; |
|||
} |
|||
|
|||
&__right-icon { |
|||
&__item { |
|||
margin-left: 10rpx; |
|||
} |
|||
|
|||
&--select { |
|||
transition: transform 0.4s; |
|||
|
|||
&--reverse { |
|||
transform: rotate(-180deg); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue