首次提交项目代码

This commit is contained in:
zhouyanli
2026-07-24 09:14:45 +08:00
commit 1d7e076242
879 changed files with 120167 additions and 0 deletions

4
huakai-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
unpackage/
.hbuilderx/
.DS_Store

73
huakai-app/App.vue Normal file
View File

@@ -0,0 +1,73 @@
<script setup>
import { onLaunch } from '@dcloudio/uni-app'
// 应用启动时执行
onLaunch(() => {
console.log('App 启动')
// #ifdef APP-PLUS
// 只有 App 平台才获取 CID
getPushCID()
// #endif
})
// 获取 uniPush 2.0 CID
function getPushCID() {
uni.getPushClientId({
success: (res) => {
console.log('推送初始化成功')
console.log('CID =', res.cid)
// 存起来备用
uni.setStorageSync('push_cid', res.cid)
// 弹出CID给你看
// uni.showModal({
// title: '你的CID',
// content: res.cid
// })
},
fail: (err) => {
console.error('获取推送 CID 失败', err)
}
})
}
// export default {
// on
// onShow: function() {
// // console.log('App Show')
// },
// onHide: function() {
// console.log('App Hide')
// },
// }
</script>
<style lang="scss">
@import "@/uni_modules/uview-plus/index.scss";
/*每个页面公共css */
</style>
<style>
/* 全局给 uni.showToast 加内边距 */
.uni-toast {
padding: 5px !important;
/* 上下20 左右40,自己改数值 */
min-height: auto !important;
box-sizing: border-box !important;
}
/* 文字也可以居中、调整行高 */
.uni-toast-content {
/* line-height: 1.6 !important; */
padding: 0 !important;
white-space: normal !important;
word-break: break-all !important;
}
/* button:after{
border-radius: 50px;
} */
</style>

View File

@@ -0,0 +1,9 @@
<template>
</template>
<script>
</script>
<style>
</style>

20
huakai-app/index.html Normal file
View File

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

24
huakai-app/main.js Normal file
View File

@@ -0,0 +1,24 @@
import App from './App'
import uviewPlus from '@/uni_modules/uview-plus'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
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(uviewPlus)
return {
app
}
}
// #endif

132
huakai-app/manifest.json Normal file
View File

@@ -0,0 +1,132 @@
{
"name" : "花开物业",
"appid" : "__UNI__29C3D60",
"description" : "物业用户端app",
"versionName" : "1.0.1",
"versionCode" : "103",
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* 模块配置 */
"modules" : {
"Geolocation" : {},
"Maps" : {},
"Camera" : {},
"Contacts" : {},
"Gallery" : {},
"Push" : {}
},
/* 应用发布信息 */
"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\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>"
]
},
/* ios打包配置 */
"ios" : {
"dSYMs" : false,
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "需要访问您的相册,用于选择图片上传",
"NSPhotoLibraryAddUsageDescription" : "保存二维码图片到相册"
}
},
/* SDK配置 */
"sdkConfigs" : {
"geolocation" : {
"system" : {
"__platform__" : [ "ios", "android" ]
}
},
"maps" : {
"amap" : {
"name" : "玉米汁穿过三峡人家",
"appkey_ios" : "39a4c28a01c3ec15684b21d4093aad3d",
"appkey_android" : "044055aed47cb305e86419fd82e4c5f3"
}
},
"push" : {
"unipush" : {
"version" : "2",
"offline" : true,
"icons" : {
"small" : {
"mdpi" : "D:/图片文档/logo-app.png"
}
}
}
},
"share" : {
"weixin" : {
"appid" : "",
"UniversalLinks" : ""
}
}
}
}
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
"mp-weixin" : {
"appid" : "wx40625124315d2de1",
"setting" : {
"urlCheck" : false
},
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ],
"permission" : {
"scope.camera" : {
"desc" : "用于扫码"
},
"scope.userLocation" : {
"desc" : "你的位置信息将用于定位选择"
}
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"app-harmony" : {
"distribute" : {
"bundleName" : "com.chengkong.property.owner"
}
}
}

74
huakai-app/package-lock.json generated Normal file
View File

@@ -0,0 +1,74 @@
{
"name": "property-uniapp-project",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"clipboard": "^2.0.11",
"dayjs": "^1.11.20",
"moment": "^2.30.1",
"uqrcodejs": "^4.0.7"
}
},
"node_modules/clipboard": {
"version": "2.0.11",
"resolved": "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz",
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"license": "MIT",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/dayjs": {
"version": "1.11.20",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"license": "MIT"
},
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"license": "MIT"
},
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"license": "MIT",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/select/-/select-1.1.2.tgz",
"integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
"license": "MIT"
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"license": "MIT"
},
"node_modules/uqrcodejs": {
"version": "4.0.7",
"resolved": "https://registry.npmmirror.com/uqrcodejs/-/uqrcodejs-4.0.7.tgz",
"integrity": "sha512-84+aZmD2godCVI+93lxE3YUAPNY8zAJvNA7xRS7R7U+q57KzMDepBSfNCwoRUhWOfR6eHFoAOcHRPwsP6ka1cA==",
"license": "Apache-2.0"
}
}
}

8
huakai-app/package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"clipboard": "^2.0.11",
"dayjs": "^1.11.20",
"moment": "^2.30.1",
"uqrcodejs": "^4.0.7"
}
}

View File

@@ -0,0 +1,141 @@
import request,{get,post,upload} from "./request.js"
// =======账号密码登录========
export const getLoginPassword = (data) =>{
return post('/api/resident/auth/loginByPassword',data)
}
// 注册新用户获取验证码
export const getSendCode = (data) =>{
return post('/api/resident/auth/sendSmsCode',data)
}
// 注册新用户
export const getRegisterUser = (data) =>{
return post('/api/resident/auth/register',data)
}
// 短信验证码登录
export const getLoginBySms = (data) =>{
return post('/api/resident/auth/loginBySms',data)
}
// 忘记密码接口
export const getResetPassword = (data) =>{
return post('/api/resident/auth/resetPassword',data)
}
// =========首页=======
// 获取bannar图
export const getBannarList = (data) =>{
return get('/resident/banner',data)
}
// 首页最新公告
export const getNoticeLatest = () =>{
return get('/api/resident/notice/latest')
}
// 查询已认证的小区
export const getVillageList = (data) =>{
return get('/api/resident/village/myList', data)
}
// 获取当前小区
export const getCurrentVillage = () =>{
return get('/api/resident/village/current')
}
// 切换小区
export const getVillageSwitch = (data)=>{
return post('/api/resident/village/switch',data)
}
// 首页最新活动列表
export const getActivitylatest = () =>{
return get('/api/resident/activity/latest')
}
// ========公告列表======
// 获取公告列表
export const getNoticeList = (data) =>{
return get('/api/resident/notice/list',data)
}
// 获取公告详情
export const getNoticeDetails = (noticeId) =>{
return get(`/api/resident/notice/${noticeId}`)
}
// =========键开门===============
// 查询门禁列表
export const getAccessDevices = (data) =>{
return get("/api/resident/access/devices",data)
}
// 确认开门
export const getAccessOpen = (data) =>{
return post("/api/resident/access/open",data)
}
// ==========活动列表===========
// 获取活动列表
export const getActivityList = (data) =>{
return get("/api/resident/activity/list",data)
}
// 获取活动列表详情
export const getActivityDetails = (id) =>{
return get(`/api/resident/activity/${id}`)
}
// 确认报名
export const confirmResident = (id) =>{
return post(`/api/resident/activity/${id}/apply`)
}
// 取消报名
export const cancelActivity = (id) =>{
return post(`/api/resident/activity/${id}/cancel`)
}
// =========我的活动========
export const getMyActivityList = (data) =>{
return get("/api/resident/activity/my",data)
}
// ===========活动实况============
// 活动实况列表
export const getActivityLive = (id) =>{
return get(`/api/resident/activity/${id}/live`)
}
// 添加实况
export const addActivityLive = (id, data) =>{
return post(`/api/resident/activity/${id}/live`, data)
}
// 签到
export const signinActivityLive = (id, data) =>{
return post(`/api/resident/activity/${id}/signin`, data)
}
// ==============在线报修========
// 添加报修工单
export const addQueryRepair = (data) =>{
return post("/repairOrder/addRepair",data)
}
// 获取公共维修项目列表
export const getRepairProjectList = (data) =>{
return get("/repairOrder/list",data)
}
// 获取报修列表
export const getQueryRepair = (data) =>{
return get("/repairOrder/queryRepair",data)
}
// 获取房屋列表
export const getHouseList = (data) =>{
return get('/api/resident/my/house/list',data)
}
// 报修详情
export const getRepairDetail = (data) =>{
return get('/repairOrder/queryRepairDetail',data)
}
// =========访客邀请===========
// 获取访客列表
export const getVisitorList = (data) =>{
return get('/api/visitor/list',data)
}
// 获取详情信息
export const getVisitorDetail = (id) =>{
return get(`/api/visitor/${id}`)
}
// 新增访客
export const addVisitorData = (data) =>{
return post("/api/visitor", data)
}
// 上传单张图片
export const uploadFile = (filePath) => {
return upload(filePath,'/api/resident/file/uploadImage');
}

View File

@@ -0,0 +1,225 @@
import request, {
get,
post,
deleteMethod,
putMethod
} from "./request.js"
// 字典
export const getSystemDictData = (data) => {
return get('/system/dict/data/list', data)
}
// =======公告列表========
export const getResidentNoticeList = (data) => {
return get('/api/resident/notice/list', data)
}
// 我的房屋列表
export const getMyHouseList = (data) => {
return get('/api/resident/my/house/list', data)
}
// 提交房屋注册认证申请
export const myHouseApply = (data) => {
return post('/api/resident/my/house/apply', data)
}
// 查询小区列表
export const getVillageList = (data) => {
return get('/api/resident/village/list', data)
} // 查询楼栋列表
export const getHouseBuildings = (data) => {
return get('/api/resident/house/buildings', data)
}
// 查询单元列表
export const getHouseUnits = (data) => {
return get('/api/resident/house/units', data)
}
// 查询楼层列表
export const getHouseFloors = (data) => {
return get('/api/resident/house/floors', data)
}
// 查询户号列表
export const getHouseHouses = (data) => {
return get('/api/resident/house/houses', data)
}
// 房屋绑定手机获取验证码
export const getHouseAuthSendSmsCode = (data) => {
return post('/api/resident/auth/sendSmsCode', data)
}
// 房屋详情
export const getMyHouseDetail = (data) => {
return get(`/api/resident/my/house/${data.id}`, data)
}
// 删除房屋
export const deleteMyHouse = (data) => {
return deleteMethod(`/api/resident/my/house/${data.id}`, data)
}
// 设置默认房屋
export const setMyHouseDefault = (data) => {
return post(`/api/resident/my/house/default`, data)
}
// 获取当前用户登录信息
export const getUserProfile = (data) => {
return get(`/api/resident/user/profile`, data)
}
// 发送新手机号验证码
export const getAuthSendSmsCode = (data) => {
return post(`/api/resident/auth/sendSmsCode`, data)
}
// 更改手机号
export const getUserChangePhone = (data) => {
return post(`/api/resident/user/changePhone`, data)
}
// 获取消息通知开关
export const getUserNotify = (data) => {
return get(`/api/resident/user/notify`, data)
}
// 更新消息通知开关
export const changeUserNotify = (data) => {
return post(`/api/resident/user/notify`, data)
} // 修改密码(需登录)
export const authChangePassword = (data) => {
return post(`/api/resident/auth/changePassword`, data)
} // 退出登录
export const authLogout = (data) => {
return post(`/api/resident/auth/logout`, data)
}
// 编辑房屋
export const upDateMyHouse = (data) => {
return putMethod(`/api/resident/my/house/${data.id}`, data)
}
// =========问卷调查=========
// 问卷列表
export const getQuestionnaireList = (data) => {
return get('/api/resident/questionnaire/list',data)
}
// 问卷详情
export const getQuestionnaireDetail = (data) => {
return get('/api/resident/questionnaire/myAnswers',data)
}
// 提交问卷
export const submitQuestionnair = (id,data) => {
return post(`/api/resident/questionnaire/${id}/submit`, data)
}
// =====投诉意见=======
// 获取投诉类型
export const getComplainType = () => {
return get('/complain/getComplainType')
}
// 获取投诉列表
export const getComplainList = (data) => {
return get('/complain/getComplainList',data)
}
// 提交投诉建议
export const addComplain = (data) => {
return post('/complain/add',data)
}
// 投诉详情
export const getComplainDetail = (data) => {
return get('/complain/detail',data)
}
// 联系物业
export const getManagePhoneData = (data) => {
return get('/api/manage/banner/getManagePhone',data)
}
// 修改身份证照片
export const uploadFileUploadImage = (data) => {
return post(`/api/resident/file/uploadImage`, data)
}
// 水电表
export const getTopUpSelectByResident = (data) => {
console.log(data);
return get(`/api/topUp/selectByResident/${data.residentUserId}`, data)
}
// 绑定设备
export const getTopUpBindDevice = (data) => {
return post(`/api/topUp/bindDevice/${data.deviceCode}/${data.residentUserId}/${data.openid}/${data.deviceType}`,
data)
}
// 充值调用
// /${data.rechargeAmount}/${data.type}/${data.deviceCode}
export const getTopUp = (data) => {
return post(`/api/topUp`, data)
}
export const getopenIdByCode = (data) => {
return get(`/api/topUp/getOpenIdByCode?code=${data.code}`, data)
}
// 微信支付调用
export const getToWxPay = (data) => {
return post(`/api/topUp/wxPay/${data.orderId}/${data.openid}`, data)
}
// 缴费记录查询
export const getTopupRecordWithStat = (data) => {
return get(
`/api/topUp/topupRecordWithStat?residentUserId=${data.residentUserId}&pageNum=${data.pageNum}&pageSize=${data.pageSize}`,
data)
}
export const postInsertOpenId = (data) => {
return post(`/api/resident/auth/insertOpenId`, data)
}
export const postSubscribeAuth = (data) => {
return post(`/api/wx/subscribeAuth/auth`, data)
}
// 添加车辆
export const postCar = (data) => {
return post(`/api/resident/car`, data)
}
// 修改车辆
export const putCar = (data) => {
return putMethod(`/api/resident/car`, data)
}
// 获取车辆列表
export const getMyCarList = (data) => {
return get(`/api/resident/car/myList`, data)
}
// 获取车位列表
export const getCarList = (data) => {
return get(`/api/resident/car/list`, data)
}
// 获取车辆详细信息
export const getCarDetail = (data) => {
return get(`/api/resident/car/${data.id}`, data)
}
// 删除车辆信息
export const deleteCar = (data) => {
return deleteMethod(`/api/resident/car/${data.carId}`, data)
}
// 获取用户车位绑定列表
export const getUserParkingSpaceList = (data) => {
return get(`/api/resident/car/parking/list`, data)
}
// 获取区域列表
export const getResidentCarAreaManageAreaListByVillageId = (data) => {
return get(`/ResidentCarAreaManage/areaListByVillageId`, data)
}
// 根据区域id获得车位列表
export const getResidentCarAreaManageParkingListByAreaId = (data) => {
return get(`/ResidentCarAreaManage/parkingListByAreaId`, data)
}
// 绑定车位
export const postCarBind = (data) => {
return post(`/api/resident/car/bind`, data)
}
// 获取用户下车位绑定信息
// export const getCarList = (data) => {
// return get(`/api/resident/car/list`, data)
// }
// 删除车位
export const deleteParking = (data) => {
return deleteMethod(`/ResidentCarAreaManage/parking/${data.parkingId}`, data)
}
// 车位详情
export const getParkingDetail = (data) => {
return get(`/api/resident/car/parking/detail/${data.id}`, data)
}
// 解绑车辆
export const rebindCar = (data) => {
return post(`/ResidentCarAreaManage/rebind`, data)
}

View File

@@ -0,0 +1,85 @@
/**
* 登录状态管理工具
* 仅在用户点击需要身份认证的功能时才要求登录
*/
/**
* 检查用户是否已登录
* @returns {boolean} true 已登录,false 未登录
*/
export function isLoggedIn() {
const token = uni.getStorageSync('token');
return !!token;
}
/**
* 要求登录 — 未登录时跳转登录页,已登录直接执行回调
* 登录页提供返回/取消按钮,用户可拒绝登录
*
* @param {Object} options
* @param {Function} [options.onLoggedIn] - 已登录或登录成功后执行的回调
* @param {string} [options.title] - 展示给用户的提示标题(为什么需要登录)
* @param {Function} [options.onCancel] - 用户取消登录后的回调
*/
export function requireAuth({ onLoggedIn, title, onCancel } = {}) {
if (isLoggedIn()) {
if (onLoggedIn) onLoggedIn();
return;
}
// 弹窗询问用户是否去登录,用户可拒绝("暂不")
const tipTitle = title ? `"${title}"需要登录` : '该功能需要登录';
uni.showModal({
title: '提示',
content: `${tipTitle},是否去登录?`,
confirmText: '去登录',
cancelText: '暂不',
success: (modalRes) => {
if (modalRes.confirm) {
// 用户点击"去登录"
uni.setStorageSync('pendingAuthCallback', {
time: Date.now(),
title: title || ''
});
uni.navigateTo({
url: '/pageSubPack/loginSub/pwd-login',
events: {
loginSuccess: () => {
if (onLoggedIn) onLoggedIn();
uni.removeStorageSync('pendingAuthCallback');
}
}
});
} else {
// 用户点击"暂不",留在当前页
if (onCancel) onCancel();
}
}
});
}
/**
* 清除登录状态
*/
export function clearLoginState() {
uni.removeStorageSync('token');
uni.removeStorageSync('userId');
uni.removeStorageSync('currentVillageId');
uni.removeStorageSync('pendingAuthCallback');
}
/**
* 保存登录成功后的 token 和用户信息
* @param {Object} data - 登录接口返回的 data
*/
export function saveLoginData(data) {
if (data.token) {
uni.setStorageSync('token', data.token);
}
if (data.userId) {
uni.setStorageSync('userId', data.userId);
}
const currentVillageId = data.currentVillageId || 0;
uni.setStorageSync('currentVillageId', currentVillageId);
}

View File

@@ -0,0 +1,113 @@
let baseUrl = '';
if (process.env.NODE_ENV === 'development') {
// baseUrl = 'https://wuyeapi.ckdzkj.com'; // 开发环境
baseUrl = "https://wuyeapi.bugtc.com"
} else {
baseUrl = 'https://wuyeapi.bugtc.com'; // 生产环境
}
/**
* 显示接口错误提示(自动处理长消息截断问题)
* @param {string|Object} msg - 错误消息或含 msg 字段的错误对象
*/
export function showApiError(msg) {
const message = typeof msg === 'string' ? msg : (msg.msg || msg.message || '请求失败');
uni.showToast({ title: message || '请求失败', icon: 'error', duration: 2000 });
}
export default function request(url, data = {}, method = "GET") {
return new Promise((resolve, reject) => {
uni.showLoading({
title: '请求中...',
mask: true
});
uni.request({
url: baseUrl + url,
data: data,
method: method,
header: {
"Authorization": "Bearer " + (uni.getStorageSync("token") || ''),
"clientid": "resident",
'content-type': 'application/json'
},
success: (res) => {
uni.hideLoading();
// HTTP 失败
if (res.statusCode !== 200) {
reject({ msg: `请求异常 ${res.statusCode}`, code: res.statusCode });
return;
}
const resData = res.data || {};
// token 过期 — 清除登录态,提示用户,但不强制跳转(用户可继续浏览非敏感页面)
if (resData.code === 401) {
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 2000 });
uni.removeStorageSync('token');
uni.removeStorageSync('userId');
// 使用 navigateTo 而非 reLaunch,用户可通过返回按钮退出登录页
setTimeout(() => {
uni.navigateTo({
url: '/pageSubPack/loginSub/pwd-login',
fail: () => {
// 如果 navigateTo 失败(如页面栈满),则用 reLaunch
uni.reLaunch({ url: '/pageSubPack/loginSub/pwd-login' });
}
});
}, 1500);
reject({ msg: '登录过期', code: 401 });
return;
}
// 成功
if (resData.code === 200) {
resolve(resData);
}
// 业务失败(code === 500 统一 showToast 展示完整 msg)
else {
if (resData.code === 500 && resData.msg) {
uni.showToast({ title: resData.msg, icon: 'error', duration: 2500 });
}
reject({ msg: resData.msg || '请求失败', code: resData.code, data: resData });
}
},
fail: (err) => {
uni.hideLoading();
reject({ msg: '网络异常', code: -1, raw: err });
}
});
});
}
export const get = (url, data = {}) => request(url, data, 'GET');
export const post = (url, data) => request(url, data, 'POST');
export const deleteMethod = (url, data) => request(url, data, 'DELETE');
export const putMethod = (url, data) => request(url, data, 'PUT');
// 上传
export const upload = (filePath, url) => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: baseUrl + url,
filePath: filePath,
name: 'file',
header: {
"Authorization": "Bearer " + (uni.getStorageSync("token") || ''),
"clientid": "resident"
},
success: (res) => {
try {
const data = JSON.parse(res.data);
resolve(data.data || ''); // 直接返回图片地址
} catch (e) {
resolve('');
}
},
fail: (err) => {
reject(err);
}
});
});
};

View File

@@ -0,0 +1,190 @@
// 服务器地址(与 request.js 保持一致)
const baseUrl = 'http://192.168.0.239:8080';
/**
* 图片上传 Hook
* 支持 uniapp 和微信小程序
* 解决编辑回显后重新上传的问题
*/
import { reactive } from 'vue'
/**
* @param {Object} options 配置
* @param {string} options.uploadUrl - 上传接口地址
* @param {string} options.fileKey - 上传文件的字段名,默认 'file'
* @returns {Object}
*/
export function useImageUpload(options = {}) {
const {
uploadUrl = '/api/resident/file/uploadImage',
fileKey = 'file'
} = options
// 使用 reactive 对象存储所有状态
const state = reactive({
// 当前显示的图片(前端展示用,支持 base64/blob URL/网络URL)
previewUrl: '',
// 服务器返回的 URL(提交时使用)
serverUrl: '',
// 临时的文件路径(重新上传时用)
tempFilePath: null,
// 上传状态
uploading: false
})
/**
* 设置回显图片(编辑时从后端获取)
* @param {string} url - 后端返回的图片URL
*/
const setPreviewUrl = (url) => {
if (url) {
// 确保 url 是字符串
const urlStr = typeof url === 'string' ? url : (url?.url || String(url))
state.previewUrl = urlStr
state.serverUrl = urlStr
state.tempFilePath = null // 重置临时文件,表示没有重新上传
}
}
/**
* 选择图片并预览(不上传)
* @returns {Promise<string>} 返回临时文件路径
*/
const chooseImage = () => {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: (res) => {
const path = res.tempFilePaths?.[0]
if (!path) {
reject(new Error('选择图片失败'))
return
}
// 确保 path 是字符串
const pathStr = typeof path === 'string' ? path : String(path)
// 设置预览
state.previewUrl = pathStr
// 记录临时文件路径
state.tempFilePath = pathStr
// 置空服务器URL,等实际上传后再获取
state.serverUrl = ''
resolve(pathStr)
},
fail: (err) => {
reject(err)
}
})
})
}
/**
* 上传图片到服务器
* @returns {Promise<Object>} 返回 { url: string }
*/
const uploadImage = () => {
// 如果没有新选择临时文件,直接返回已有的服务器URL
if (!state.tempFilePath) {
return Promise.resolve({ url: state.serverUrl })
}
// 如果已经有服务器URL且没有重新选择,也直接返回
if (state.serverUrl && !state.tempFilePath) {
return Promise.resolve({ url: state.serverUrl })
}
state.uploading = true
return new Promise((resolve, reject) => {
// 确保 filePath 是字符串
const filePath = state.tempFilePath
if (!filePath || typeof filePath !== 'string') {
state.uploading = false
reject(new Error('文件路径无效'))
return
}
uni.uploadFile({
url: baseUrl + uploadUrl,
filePath: filePath,
name: fileKey,
header: {
'Authorization': 'Bearer ' + (uni.getStorageSync('token') || ''),
'clientid': 'resident'
},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.code === 200 && data.data) {
// 更新服务器URL(确保存的是字符串)
const url = typeof data.data === 'string' ? data.data : (data.data?.url || '')
state.serverUrl = url
// 清空临时文件
state.tempFilePath = null
resolve({ url })
} else {
uni.showToast({
title: data.msg || '上传失败',
icon: 'none'
})
reject(new Error(data.msg || '上传失败'))
}
} catch (e) {
uni.showToast({
title: '上传失败',
icon: 'none'
})
reject(e)
}
},
fail: (err) => {
uni.showToast({
title: err.message || '上传失败',
icon: 'none'
})
reject(err)
},
complete: () => {
state.uploading = false
}
})
})
}
/**
* 获取最终可提交的URL
* 如果有新上传的图片,先上传再返回URL
* 如果没有新上传,直接返回已有的服务器URL
* @returns {Promise<Object>}
*/
const getSubmitUrl = async () => {
// 如果重新选择了新图片,先上传
if (state.tempFilePath) {
return await uploadImage()
}
// 否则返回已有的服务器URL
return { url: state.serverUrl }
}
/**
* 重置状态
*/
const reset = () => {
state.previewUrl = ''
state.serverUrl = ''
state.tempFilePath = null
state.uploading = false
}
// 返回 reactive state 和方法
// state 作为 reactive 对象,在模板中通过 idCardFrontUpload.state.previewUrl 访问
return {
state,
setPreviewUrl,
chooseImage,
uploadImage,
getSubmitUrl,
reset
}
}

View File

@@ -0,0 +1,207 @@
<template>
<view class="reset-pwd-page">
<view class="status_bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">修改密码</view>
</view>
<!-- 密码提示 -->
<view class="pwd-tips">密码为 6-16 位,需同时包含字母和数字</view>
<!-- 新密码输入框 -->
<view class="input-item">
<uni-easyinput class="input-content" type="password" v-model="newPassword"
placeholder="请输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 确认密码输入框 -->
<view class="input-item">
<uni-easyinput class="input-content" type="password" v-model="confirmPassword"
placeholder="请再次输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 确定按钮 -->
<button
class="confirm-btn"
:disabled="!canConfirm"
@click="handleConfirm"
>
确定
</button>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import {onLoad,onShow} from '@dcloudio/uni-app'
import{getResetPassword} from '../api/api.js'
import { showApiError } from '../api/request.js'
// ========== 响应式数据 ==========
// 新密码
const newPassword = ref('');
// 确认密码
const confirmPassword = ref('');
const placeholderStyle = ref('font-size:24rpx')
// 定义接收上一页传来的参数
const phone = ref('');
const smsCode = ref('');
// ========== 计算属性 ==========
// 是否可点击确定按钮(密码格式正确 + 两次输入一致)
const canConfirm = computed(() => {
// 密码格式规则:6-16位任意字符
const pwdReg = /^(?=.*[a-zA-Z])(?=.*\d).{6,16}$/;
// 校验条件:新密码符合格式 + 确认密码和新密码一致 + 均不为空
return pwdReg.test(newPassword.value)
&& newPassword.value === confirmPassword.value
&& newPassword.value && confirmPassword.value;
});
// ========== 方法 ==========
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
onShow(() => {
// 从缓存读取参数
const params = uni.getStorageSync('tempChangePwdParams') || {};
// 赋值
phone.value = params.phoneNumber
smsCode.value = params.verifyCode
// 用完删除缓存(避免下次跳转读取旧值)
uni.removeStorageSync('tempChangePwdParams');
});
// 检查表单状态(实时更新按钮状态)
const checkForm = () => {};
// 处理确定按钮点击(修改密码逻辑)
const handleConfirm = () => {
if (!canConfirm.value) return
let params = {
phone:phone.value,
smsCode:smsCode.value,
newPassword:newPassword.value,
confirmPassword:confirmPassword.value,
scene:'reset'
}
getResetPassword(params).then(res =>{
if(res.code === 200){
//uni.setStorageSync('token', res.token); // 假设后端返回token字段
uni.showToast({ title: '修改成功', icon: 'success' });
setTimeout(() =>{
uni.navigateTo({ url: '/pageSubPack/loginSub/pwd-login' });
},1500)
}
// else {
// uni.showToast({ title: "修改失败", icon: 'error' });
//}
}).catch(err =>{
showApiError(err)
})
};
</script>
<style scoped>
/* 页面容器 */
.reset-pwd-page {
padding: 20rpx 40rpx 40rpx;
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar{
width: 100%;
height: 46px;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
height: 88rpx;
margin-bottom: 60rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
}
/* 输入框通用样式 */
.input-item {
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
background-color: transparent !important;
}
.input-content :deep(.uni-easyinput__content-input) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
line-height: 88rpx;
}
/* 密码提示 */
.pwd-tips {
font-size: 24rpx;
color: #999;
margin-bottom: 20rpx;
padding-left: 10rpx;
}
/* 确定按钮 */
.confirm-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-top: 20rpx;
margin-top: 66rpx;
}
/* 确定按钮禁用状态 */
.confirm-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
/* 禁用按钮边框 */
button:after{
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,253 @@
<template>
<view class="forget-page">
<view class="status_bar"></view>
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">忘记密码</view>
</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber" trim="all"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 验证码输入 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode" trim="all"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
>
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<!-- 下一步 -->
<button
class="forget-btn"
:disabled="!canRegister"
@click="handleNext"
>
下一步
</button>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getSendCode} from '../api/api.js'
import { showApiError } from '../api/request.js'
// ====响应式数据======
const phoneNumber = ref('')
const verifyCode = ref('')
const countDown = ref(0)
let timer = null;
const placeholderStyle = ref('font-size:24rpx')
// ========== 计算属性 ==========
// 是否可获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'reset'
}
getSendCode(params).then(res =>{
if(res.code === 200){
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
// }else{
// uni.showToast({
// title: '验证码发送失败',
// icon: 'error'
// });
}
}).catch(err =>{
showApiError(err)
})
};
// 是否下一步
const canRegister = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/; // 手机号11位
const verifyReg = /^\d{6}$/; // 验证码6位
return phoneReg.test(phoneNumber.value)
&& verifyReg.test(verifyCode.value)
});
// 下一步
const handleNext = () =>{
// 把参数存到本地缓存
uni.setStorageSync('tempChangePwdParams', {
phoneNumber:phoneNumber.value,
verifyCode: verifyCode.value
});
if(!canRegister.value){
uni.showToast({ title: '请输入',icon: 'error'});
return
}
uni.navigateTo({
url: '/pageSubPack/loginSub/change-pwd'
});
}
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
</script>
<style scoped>
.forget-page{
padding: 20rpx 40rpx 40rpx;
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar{
width: 100%;
height: 46px;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
/* height: 88rpx; */
margin-bottom: 70rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
margin-right: 100rpx;
}
/* 输入框通用样式 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 验证码输入项特殊布局 */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框图标 */
.input-icon {
margin-right: 20rpx;
color: #999;
width: 50rpx;
height: 50rpx;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
background-color: transparent !important;
}
.input-content :deep(.uni-easyinput__content-input) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
line-height: 88rpx;
}
.verify-input {
padding-right: 20rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
}
.get-verify-btn::after{
border-radius: 50px;
}
/* 下一步按钮 */
.forget-btn{
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 40rpx;
margin-top: 66rpx;
}
.forget-btn:disabled{
background-color: #99c8ff;
color: #fff;
}
/* 禁用按钮边框 */
.forget-btn:after{
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,403 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<!-- 自定义导航栏:返回按钮 — 用户可拒绝登录 -->
<view class="custom-nav">
<view class="back-btn" @click="handleCancel">
<uni-icons type="arrow-left" size="24" color="#333"></uni-icons>
<text class="back-text">返回</text>
</view>
</view>
<!-- 登录标题 -->
<view class="login-title">手机验证码登录</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="phone" size="24" color="#999"></uni-icons> -->
<image src="https://wuyeadmin.bugtc.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 验证码输入框 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
>
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<!-- 协议勾选 -->
<view class="agreement-item" >
<view @click="toggleAgreement">
<uni-icons
type="checkbox-filled"
size="24"
color="#007aff"
v-if="agreeAgreement"
></uni-icons>
<uni-icons
type="checkbox"
size="24"
color="#999"
v-else
></uni-icons>
</view>
<text class="agreement-text" @click.stop="goToPrivacy">我已阅读并同意<text class="agreePrivacy" @click.stop="goToPrivacy">《用户隐私政策》</text></text>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
@click="handleLogin"
>
登录
</button>
<!-- 底部链接 -->
<view class="bottom-link">
<navigator class="link-item" url="/pageSubPack/loginSub/pwd-login">账号密码登录</navigator>
<navigator class="link-item" url="/pageSubPack/loginSub/register-new-user">注册新用户</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getLoginBySms,getSendCode} from '../api/api.js'
import { showApiError } from '../api/request.js'
import { saveLoginData } from '../api/auth.js'
// ========== 响应式数据 ==========
// 手机号
const phoneNumber = ref('');
// 验证码
const verifyCode = ref('');
const placeholderStyle = ref('font-size:24rpx')
// 是否同意协议
const agreeAgreement = ref(false); // 默认不勾选
// 倒计时(秒)
const countDown = ref(0);
// 定时器
let timer = null;
// ========== 计算属性 ==========
// 是否可以获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
// 手机号正则:11位数字,以1开头
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
// 是否可以登录(手机号+验证码+协议都满足)
const canLogin = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
const verifyReg = /^\d{6}$/;
return phoneReg.test(phoneNumber.value) && verifyReg.test(verifyCode.value) && agreeAgreement.value;
});
// ========== 方法 ==========
// 用户取消登录 — 返回上一页
const handleCancel = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({ url: '/pages/paymentIndex/paymentIndex' });
}
};
// 检查表单状态
const checkForm = () => {
// 无需额外逻辑,计算属性已实时监听
};
// 切换协议勾选状态
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 跳转隐私协议
const goToPrivacy = () => {
uni.navigateTo({
url:'/pageSubPack/loginSub/privacy',
})
}
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'login'
}
uni.showLoading({ title: '发送中...', mask: true });
getSendCode(params).then(res =>{
if(res.code === 200){
uni.hideLoading();
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
}
}).catch(err => {
showApiError(err)
})
};
// 登录处理
const handleLogin = () => {
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
}
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
if (!verifyCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
if (!/^\d{6}$/.test(verifyCode.value)) {
uni.showToast({ title: '验证码为 6 位数字', icon: 'none' });
return;
}
if (!agreeAgreement.value) {
uni.showToast({ title: '请勾选协议', icon: 'none' });
return;
}
uni.showLoading({ title: '登录中...', mask: true });
let params = {
phone:phoneNumber.value,
smsCode:verifyCode.value,
scene:'login',
}
getLoginBySms(params).then(res =>{
if(res.code === 200){
if(res.data){
saveLoginData(res.data);
uni.hideLoading();
uni.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() =>{
// 登录成功后返回来源页
uni.navigateBack({
fail: () => {
uni.switchTab({ url: '/pages/paymentIndex/paymentIndex' });
}
});
},1500)
}
}
}).catch(err => {
showApiError(err)
})
uni.hideLoading()
};
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style scoped>
/* 页面容器 */
.login-page {
padding: 80rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(to bottom, #E2F0FF, #F6FAFF );
}
.status_bar {
height: var(--status-bar-height, 88rpx);
width: 100%;
}
/* ===== 自定义导航:返回/取消按钮 ===== */
.custom-nav {
margin-bottom: 20rpx;
}
.back-btn {
display: inline-flex;
align-items: center;
padding: 10rpx 20rpx;
background: rgba(255,255,255,0.9);
border-radius: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
}
.back-text {
font-size: 28rpx;
color: #333;
margin-left: 6rpx;
}
/* 登录标题 */
.login-title {
font-size: 52rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 80rpx;
}
/* 输入框项 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 验证码输入项(特殊布局) */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框图标 */
.input-icon {
width: 50rpx;
height: 50rpx;
margin-right: 20rpx;
color: #999;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
background-color: transparent !important;
}
.input-content :deep(.uni-easyinput__content-input) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
line-height: 88rpx;
}
/* 验证码输入框 */
.verify-input {
padding-right: 20rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
/* 获取验证码按钮禁用状态 */
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
border-radius: 50px;
}
.get-verify-btn::after{
border-radius: 50px;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 70rpx 0 28rpx 0;
padding-left: 10rpx;
}
/* 协议文本 */
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
}
.agreePrivacy{
color: #2F77FD;
}
/* 登录按钮 */
.login-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 60rpx;
}
/* 登录按钮禁用状态 */
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
/* 禁用按钮边框 */
button:after{
border-radius: 8px;
}
/* 底部链接 */
.bottom-link {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
}
/* 链接项 */
.link-item {
font-size: 28rpx;
color: #007aff;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<view class="privacy-page">
<!-- 协议内容 -->
<scroll-view class="privacy-content" scroll-y>
<view class="content-wrap">
<view class="title">隐私政策</view>
<view class="section">
<!-- <view class="section-title">一、适用范围</view> -->
<view class="section-text">
欢迎使用我们的产品/服务(以下统称“服务”)。我们高度重视用户隐私保护,
致力于为您提供安全、可靠的服务体验。本隐私政策旨在向您说明我们如何收集、使用、存储、保护您的个人信息,
以及您享有的相关权利。请您在使用我们的服务前,仔细阅读并理解本政策全部内容,
一旦您使用我们的服务,即视为您同意本政策的所有条款。
一、我们收集的个人信息 为了向您提供更优质、个性化的服务,我们会根据服务功能的需要,收集您主动提供或在使用服务过程中产生的个人信息,
具体如下: (一)您主动提供的信息 账户注册信息:当您注册我们的账户时,您可能需要提供姓名、手机号码、电子邮箱、密码等信息,用于身份验证、账户登录及找回。
服务相关信息:根据您使用的具体服务,您可能需要提供相关补充信息(如使用客服咨询服务时,提供问题描述及相关凭证;使用交易类服务时,提供收货地址、支付相关信息等)。
自愿提交的信息:您主动向我们反馈的意见、建议,或参与我们的问卷调查、活动时提交的相关信息。
若您通过第三方平台(如微信、支付宝、QQ等)登录我们的服务,我们会根据您的授权,
从第三方获取您的相关账户信息(如昵称、头像),用于完成登录及账户绑定,
我们不会获取第三方平台的其他未授权信息。
</view>
</view>
</view>
</scroll-view>
<!-- 底部同意按钮(新增) -->
<!-- <view class="agree-btn-wrap" v-if="needAgree">
<button class="agree-btn" @click="agreePrivacy">同意隐私政策</button>
</view> -->
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
// 返回上一页
// const navigateBack = () => {
// uni.navigateBack({
// delta: 1
// });
// };
// 页面加载
// onLoad((options) => {
// // 判断是否需要同意
// if (options.needAgree === '1') {
// needAgree.value = true;
// }
// });
// 返回上一页
// const goToBack = () => {
// uni.navigateBack({
// delta: 1
// });
// };
// 同意隐私政策(新增)
// const agreePrivacy = () => {
// // #ifdef MP-WEIXIN
// wx.requirePrivacyAuthorize({
// success: () => {
// uni.showToast({
// title: '已同意隐私政策',
// icon: 'success'
// });
// // 记录授权状态(可选,存入本地缓存)
// uni.setStorageSync('privacyAgreed', true);
// navigateBack();
// },
// fail: () => {
// uni.showToast({
// title: '授权失败',
// icon: 'none'
// });
// }
// });
// // #endif
// // 非微信小程序直接标记同意
// // #ifndef MP-WEIXIN
// uni.setStorageSync('privacyAgreed', true);
// uni.showToast({
// title: '已同意隐私政策',
// icon: 'success'
// });
// navigateBack();
// // #endif
// };
</script>
<style scoped>
.privacy-page {
min-height: 100vh;
background-color: #f8f8f8;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.nav-back {
display: flex;
align-items: center;
}
.back-text {
font-size: 30rpx;
color: #333;
margin-left: 10rpx;
}
.nav-title {
font-size: 32rpx;
font-weight: 500;
margin-left: 60rpx;
}
/* 协议内容 */
.privacy-content {
height: calc(100vh - 88rpx);
}
.content-wrap {
padding: 30rpx;
}
.title {
font-size: 28rpx;
font-weight: 600;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
.section {
margin-bottom: 30rpx;
padding: 0 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 10rpx;
}
.section-text {
font-size: 28rpx;
line-height: 1.6;
color: #666;
}
/* 同意按钮样式(新增) */
.agree-btn-wrap {
padding: 20rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
border-top: 1rpx solid #eee;
}
.agree-btn {
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
height: 88rpx;
font-size: 32rpx;
}
</style>

View File

@@ -0,0 +1,320 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<!-- 自定义导航栏:返回按钮 — 用户可拒绝登录 -->
<view class="custom-nav">
<view class="back-btn" @click="handleCancel">
<uni-icons type="arrow-left" size="24" color="#333"></uni-icons>
<text class="back-text">返回</text>
</view>
</view>
<!-- 标题 -->
<view class="login-title">账号密码登录</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="phone" size="24" color="#999"></uni-icons> -->
<image src="https://wuyeadmin.bugtc.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="text" v-model="phoneNumber"
placeholder="请输入帐号" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
<image src="https://wuyeadmin.bugtc.com/images/login/password.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="password" v-model="password"
placeholder="请输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 协议勾选 + 忘记密码 -->
<view class="agreement-wrap">
<view class="agreement-item">
<view @click="toggleAgreement">
<uni-icons
type="checkbox-filled"
size="24"
color="#007aff"
v-if="agreeAgreement"
></uni-icons>
<uni-icons
type="checkbox"
size="24"
color="#999"
v-else
></uni-icons>
</view>
<text class="agreement-text" @click.stop="goToPrivacy">我已阅读并同意
<text class="agreePrivacy" @click.stop="goToPrivacy">《用户隐私政策》</text>
</text>
</view>
<navigator class="forget-pwd" url="/pageSubPack/loginSub/forget-pwd">忘记密码?</navigator>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
@click="handleLogin"
>
登录
</button>
<!-- 底部链接 -->
<view class="bottom-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">验证码登录</navigator>
<navigator class="link-item" url="/pageSubPack/loginSub/register-new-user">注册新用户</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import {onLoad} from "@dcloudio/uni-app"
import {getLoginPassword} from '../api/api.js'
import { showApiError } from '../api/request.js'
import { saveLoginData } from '../api/auth.js'
// 响应式数据
const phoneNumber = ref('');
const password = ref('');
const agreeAgreement = ref(false); // 默认勾选
const placeholderStyle = ref('font-size:24rpx')
// 计算属性:是否可登录(帐号+密码+协议都满足)
const canLogin = computed(() => {
const phoneReg = /^[a-zA-Z0-9_-]{4,16}$/; // 4-16位
const pwdReg = /^.{6,16}$/; // 密码6-16位
return phoneReg.test(phoneNumber.value) && pwdReg.test(password.value) && agreeAgreement.value;
});
// 切换协议勾选
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 跳转隐私协议
const goToPrivacy = () => {
uni.navigateTo({
url:'/pageSubPack/loginSub/privacy',
})
}
// 检查表单
const checkForm = () => {};
// 用户取消登录 — 返回上一页
const handleCancel = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({ url: '/pages/paymentIndex/paymentIndex' });
}
};
onLoad(() =>{
});
// 登录逻辑
const handleLogin = () => {
if (!phoneNumber.value) {
uni.showToast({ title: '请输入账号', icon: 'none' });
return;
}
if (phoneNumber.value.length < 4 || phoneNumber.value.length > 16) {
uni.showToast({ title: '账号长度需为 4-16 位', icon: 'none' });
return;
}
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return;
}
if (password.value.length < 6) {
uni.showToast({ title: '密码至少输入 6 位', icon: 'none' });
return;
}
if (password.value.length > 16) {
uni.showToast({ title: '密码不能超过 16 位', icon: 'none' });
return;
}
if(!agreeAgreement.value){
uni.showToast({ title: '请勾选协议', icon: 'none' });
return;
}
uni.showLoading({ title: '登录中...', mask: true });
let params = {
username:phoneNumber.value,
password:password.value
}
getLoginPassword(params).then(res =>{
if(res.code === 200){
saveLoginData(res.data);
uni.hideLoading();
uni.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() =>{
// 登录成功后返回来源页
uni.navigateBack({
fail: () => {
uni.switchTab({ url: '/pages/paymentIndex/paymentIndex' });
}
});
},1000)
}
}).catch(err => {
// console.log(err)
showApiError(err)
})
};
</script>
<style scoped>
.login-page {
padding: 80rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar {
height: var(--status-bar-height, 88rpx);
width: 100%;
}
/* ===== 自定义导航:返回/取消按钮 ===== */
.custom-nav {
margin-bottom: 20rpx;
}
.back-btn {
display: inline-flex;
align-items: center;
padding: 10rpx 20rpx;
background: rgba(255,255,255,0.9);
border-radius: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
}
.back-text {
font-size: 28rpx;
color: #333;
margin-left: 6rpx;
}
.login-title {
font-size: 52rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 80rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.input-icon {
margin-right: 20rpx;
width: 50rpx;
height: 50rpx;
}
.input-content {
flex: 1;
font-size: 28rpx;
color: #333;
height: 100%;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
background-color: transparent !important;
}
.input-content :deep(.uni-easyinput__content-input) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
line-height: 88rpx;
}
.agreement-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin: 70rpx 0 28rpx 0;
padding: 0 10rpx;
}
.agreement-item {
display: flex;
align-items: center;
}
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
}
.agreePrivacy{
color: #2F77FD;
}
.forget-pwd {
font-size: 24rpx;
color: #666;
text-decoration: none;
}
.login-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 60rpx;
}
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
/* 禁用按钮边框 */
button:after{
border-radius: 8px;
}
.bottom-link {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
}
.link-item {
font-size: 28rpx;
color: #007aff;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,450 @@
<template>
<view class="register-page">
<view class="status_bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">注册</view>
</view>
<!-- 注册标题 -->
<view class="register-title">注册用户</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/phone.png" mode=""
style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber" placeholder="请输入手机号"
:inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 验证码输入 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/code.png" mode="" style="height: 100%;width: 100%;">
</image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode" placeholder="请输入验证码"
:inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button class="get-verify-btn" :disabled="!canGetVerify || countDown > 0" @click="getVerifyCode">
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<!-- 账号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/account.png" mode=""
style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="person" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content" type="text" v-model="account" placeholder="请输入账号" :inputBorder="false"
:placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/password.png" mode=""
style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content" type="password" v-model="password" placeholder="请输入密码"
:inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 协议勾选 -->
<view class="agreement-item">
<view @click="toggleAgreement">
<uni-icons type="checkbox-filled" size="24" color="#007aff" v-if="agreeAgreement"></uni-icons>
<uni-icons type="checkbox" size="24" color="#999" v-else></uni-icons>
</view>
<text class="agreement-text" @click.stop="goToPrivacy">我已阅读并同意<text
class="agreePrivacy">《用户隐私政策》</text></text>
</view>
<!-- 立即注册按钮 -->
<button class="register-btn" @click="handleRegister">
立即注册
</button>
<!-- 跳转登录链接 -->
<view class="login-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">已有账号,去登录</navigator>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
onUnmounted
} from 'vue';
import {
getRegisterUser,
getSendCode
} from '../api/api.js'
// ========== 响应式数据 ==========
// 表单数据
import { showApiError } from '../api/request.js'
const phoneNumber = ref('');
const verifyCode = ref('');
const account = ref('');
const password = ref('');
const placeholderStyle = ref('font-size:24rpx')
// 协议勾选
const agreeAgreement = ref(false);
// 验证码倒计时
const countDown = ref(0);
let timer = null;
// ========== 计算属性 ==========
// 是否可获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
// 是否可注册(所有表单项+协议都满足)
const canRegister = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/; // 手机号11位
const verifyReg = /^\d{6}$/; // 验证码6位
const accountReg = /^.{4,20}$/; // 账号4-20位
const pwdReg = /^.{6,16}$/; // 密码6-16位
return phoneReg.test(phoneNumber.value) &&
verifyReg.test(verifyCode.value) &&
accountReg.test(account.value) &&
pwdReg.test(password.value) &&
agreeAgreement.value;
});
// ========== 方法 ==========
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 切换协议勾选状态
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 跳转隐私协议
const goToPrivacy = () => {
uni.navigateTo({
url: '/pageSubPack/loginSub/privacy',
})
}
// 检查表单状态(实时更新按钮状态)
const checkForm = () => {};
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone: phoneNumber.value,
scene: 'register'
}
getSendCode(params).then(res => {
if (res.code === 200) {
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
// } else {
// uni.showToast({
// title: '验证码发送失败',
// icon: 'error'
// });
}
}).catch(err => {
showApiError(err)
})
};
// 处理注册逻辑
const handleRegister = () => {
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
}
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
if (!verifyCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
if (!/^\d{6}$/.test(verifyCode.value)) {
uni.showToast({ title: '验证码为 6 位数字', icon: 'none' });
return;
}
if (!account.value) {
uni.showToast({ title: '请输入账号', icon: 'none' });
return;
}
if (account.value.length < 4 || account.value.length > 20) {
uni.showToast({ title: '账号长度需为 4-20 位', icon: 'none' });
return;
}
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return;
}
if (password.value.length < 6) {
uni.showToast({ title: '密码至少输入 6 位', icon: 'none' });
return;
}
if (password.value.length > 16) {
uni.showToast({ title: '密码不能超过 16 位', icon: 'none' });
return;
}
if (!agreeAgreement.value) {
uni.showToast({ title: '请勾选协议', icon: 'none' });
return;
}
uni.showLoading({
title: '注册中...',
mask: true
});
let params = {
phone: phoneNumber.value,
smsCode: verifyCode.value,
username: account.value,
password: password.value
}
getRegisterUser(params).then(res => {
if (res.code === 200) {
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success'
});
setTimeout(() => {
uni.redirectTo({
url: '/pageSubPack/loginSub/pwd-login'
});
}, 1500);
// } else {
// uni.showToast({
// title: '注册失败',
// icon: 'error'
// });
}
}).catch((err) => {
showApiError(err)
})
// 注册成功后跳转到登录页
// uni.redirectTo({
// url: '/pages/login/pwd-login'
// });
// setTimeout(() => {
// }, 1500);
};
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style scoped>
/* 页面容器 */
.register-page {
padding: 20rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF);
}
.status_bar {
height: 46px;
width: 100%;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
/* height: 88rpx; */
margin-bottom: 70rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
margin-right: 100rpx;
}
/* 注册标题 */
.register-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 60rpx;
}
/* 输入框通用样式 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 验证码输入项特殊布局 */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框图标 */
.input-icon {
margin-right: 20rpx;
color: #999;
width: 50rpx;
height: 50rpx;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
background-color: transparent !important;
}
.input-content :deep(.uni-easyinput__content-input) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
line-height: 88rpx;
}
.verify-input {
padding-right: 20rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 26rpx;
padding: 0;
margin: 0;
}
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
}
.get-verify-btn::after {
border-radius: 50px;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 70rpx 0 28rpx 0;
padding-left: 10rpx;
}
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
}
.agreePrivacy {
color: #2F77FD;
}
/* 立即注册按钮 */
.register-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 40rpx;
}
button:after {
border-radius: 8px;
}
.register-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
/* 跳转登录链接 */
.login-link {
text-align: center;
}
.link-item {
font-size: 28rpx;
color: #007aff;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,545 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="'新增'+paymentType+'缴费'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="card-box">
<!-- 头部水表标题+水滴图标 -->
<view class="card-header">
<image class="iconStyle"
:src="paymentType=='水表'?'https://wuyeadmin.bugtc.com/images/propertyImgs/water.png':'https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png'">
</image>
<text class="card-title">{{paymentType}}</text>
</view>
<!-- 分割线 -->
<view class="line"></view>
<view class="input-item">
<view class="input-label">缴费单位</view>
<text class="company-name">花开物业</text>
</view>
<!-- 分割线 -->
<view class="line"></view>
<!-- 设备号输入项 -->
<view class="input-item">
<view class="input-label">缴费设备号</view>
<view class="input-wrap">
<!-- 输入框 -->
<input class="input" placeholder="请输入设备号" placeholder-class="input-placeholder"
v-model="deviceNo" />
<!-- 右边扫码图标 text标签直接用 双端不乱码 -->
<image class="scanTheCodeIconStyle"
src="https://wuyeadmin.bugtc.com/images/propertyImgs/scanTheCodeIcon.png" @click="scanCode">
</image>
</view>
</view>
<!-- 底部分割线 -->
<view class="line"></view>
</view>
<!-- 底部协议和提交按钮 -->
<view class="bottom-area">
<!-- <view class="agreement">
<checkbox-group @change="handleAgreeChange">
<checkbox :checked="isAgree" />
</checkbox-group>
<text class="agreement-text">我已阅并同意</text>
<text class="agreement-link" @click="handleProtocol">《协议链接》</text>
</view> -->
<button class="submit-btn" @click="goPay">
提交
</button>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getTopUpBindDevice,
getopenIdByCode
} from '/pageSubPack/api/apiSub.js'
import { showApiError } from '/pageSubPack/api/request.js'
// 响应式数据
const paymentType = ref(uni.getStorageSync('addPaymentType'))
const deviceNo = ref('')
const isAgree = ref(false)
// 生命周期
onMounted(() => {})
// 扫码
const scanCode = async () => {
try {
uni.showLoading({
title: '扫码中...',
mask: true
})
const res = await uni.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode']
})
// 扫码成功,赋值
deviceNo.value = res.result
uni.hideLoading()
uni.showToast({
title: '扫码成功',
icon: 'success'
})
} catch (err) {
uni.hideLoading()
console.log('扫码失败', err)
uni.showToast({
title: '扫码取消/失败',
icon: 'none'
})
}
}
// 提交缴费
const goPay = async () => {
if (!deviceNo.value) {
uni.showToast({
title: '请输入设备号',
icon: 'none'
})
return
}
uni.showModal({
title: '确认提交',
content: '确认要提交吗?',
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '提交中...',
mask: true
})
try {
// // 获取 openid
// const loginRes = await wx.login()
// if (!loginRes.code) {
// uni.hideLoading()
// uni.showToast({
// title: '登录失败',
// icon: 'none'
// })
// return
// }
// // 用 code 换 openid
// const openIdRes = await getopenIdByCode({
// code: loginRes.code
// })
// if (openIdRes.code !== 200 || !openIdRes.data) {
// uni.hideLoading()
// uni.showToast({
// title: '获取openid失败',
// icon: 'none'
// })
// return
// }
const openid = uni.getStorageSync('openid')
await getTopUpBindDevice({
deviceType: paymentType.value=='水表'?'2':'1',
deviceCode: deviceNo.value,
openid: openid,
residentUserId: uni.getStorageSync('userId')
})
uni.hideLoading()
uni.showToast({
title: '提交成功',
icon: 'success'
})
setTimeout(() => {
goNext()
}, 1500)
} catch (err) {
uni.hideLoading()
showApiError(err)
console.error('绑定设备失败:', err)
}
}
}
})
}
// 跳转成功页
const goNext = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/addPaymentSuccess'
})
}
// 返回
const goBack = () => {
uni.switchTab({
url: '/pages/paymentIndex/paymentIndex'
})
}
</script>
<style lang="scss">
page {
background: #f2f1f6;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 白色卡片容器 */
.card-box {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
}
/* 头部水表标题 */
.card-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
}
.iconStyle {
width: 88rpx;
height: 88rpx;
}
.scanTheCodeIconStyle {
width: 48rpx;
height: 48rpx;
padding-left: 20rpx;
}
.water-icon {
font-size: 32rpx;
}
.card-title {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
/* 分割线 */
.line {
width: 100%;
height: 1rpx;
background: #eeeeee;
}
/* 输入项整体 */
.input-item {
padding: 32rpx 0;
}
.input-label {
font-size: 28rpx;
color: #333;
margin-bottom: 24rpx;
}
/* 输入框+扫码图标横向布局 */
.input-wrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.input {
flex: 1;
font-size: 32rpx;
color: #333;
}
/* 占位符样式 */
.input-placeholder {
color: #cccccc;
}
/* 底部协议+按钮区域 */
.bottom-area {
margin-top: 60rpx;
padding: 0 10rpx;
}
/* 协议勾选 */
.agree-item {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 40rpx;
}
.radio-box {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.radio-icon {
font-size: 32rpx;
color: #999;
}
.agree-text {
font-size: 28rpx;
color: #666;
}
.agree-link {
font-size: 28rpx;
color: #007aff;
}
/* 立即缴费按钮 */
.pay-btn {
width: 100%;
height: 96rpx;
background: #007aff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.pay-text {
font-size: 36rpx;
color: #ffffff;
}
/* 白色卡片容器 */
.card-box {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
}
/* 头部水表标题 */
.card-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
}
.water-icon {
font-size: 48rpx;
}
.card-title {
font-size: 36rpx;
color: #333;
font-weight: 500;
}
/* 分割线 */
.line {
width: 100%;
height: 1rpx;
background: #eeeeee;
}
/* 输入项整体 */
.input-item {
padding: 32rpx 0;
}
.input-label {
font-size: 32rpx;
color: #333;
margin-bottom: 24rpx;
}
/* 输入框+扫码图标横向布局 */
.input-wrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.input {
flex: 1;
font-size: 32rpx;
color: #333;
}
/* 占位符样式 */
.input-placeholder {
color: #cccccc;
}
/* 右边扫码图标 text标签直接用 双端不乱码 */
.scan-icon {
font-size: 40rpx;
color: #999;
padding-left: 20rpx;
}
/* 底部协议+按钮区域 */
.bottom-area {
margin-top: 60rpx;
padding: 0 10rpx;
}
/* 协议勾选 */
.agree-item {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 40rpx;
}
.radio-box {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.radio-icon {
font-size: 32rpx;
color: #999;
}
.agree-text {
font-size: 28rpx;
color: #666;
}
.agree-link {
font-size: 28rpx;
color: #007aff;
}
/* 立即缴费按钮 */
.pay-btn {
width: 100%;
height: 96rpx;
background: #007aff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.pay-text {
font-size: 36rpx;
color: #ffffff;
}
.status-bar {
width: 100vw;
height: 46px;
}
/* 底部区域 */
.bottom-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 30rpx;
box-sizing: border-box;
}
.agreement {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.agreement checkbox {
transform: scale(0.8);
margin-right: 10rpx;
}
.agreement-text {
font-size: 24rpx;
color: #333;
}
.agreement-link {
font-size: 28rpx;
color: #4080ff;
margin-left: 10rpx;
}
.submit-btn {
width: 100%;
height: 90rpx;
line-height: 90rpx;
background-color: #4080ff;
color: #fff;
border-radius: 10rpx;
font-size: 36rpx;
font-weight: 500;
border: none;
outline: none;
}
.submit-btn[disabled] {
background-color: #a0c4ff;
color: #fff;
opacity: 0.7;
}
</style>

View File

@@ -0,0 +1,522 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="'新增'+paymentType+'缴费'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 顶部蓝色标题栏 -->
<view class="header-card">
<view class="header-icon">
<image class="iconStyle"
:src="paymentType=='燃气'?'https://wuyeadmin.bugtc.com/images/propertyImgs/gas.png':'https://wuyeadmin.bugtc.com/images/propertyImgs/heating.png'">
></image>
</view>
<text class="header-title">{{paymentType+'费'}}</text>
</view>
<!-- 缴费信息卡片 -->
<view class="info-card">
<view class="form-item" @click="goToChange()">
<text class="label">缴费单位</text>
<text class="company-name">XXXXXXXX有限公司</text>
</view>
<view class="form-item">
<text class="label">缴费单位</text>
<view class="input-wrapper">
<input type="number" placeholder="请输入户号" class="input-field" v-model="userNo" />
<text class="help-link" @click="showPopup = true">如何获取户号?</text>
</view>
</view>
</view>
</view>
<!-- 快速获取户号 底部弹窗 -->
<view v-if="showPopup" class="popup-mask" @click="showPopup = false">
<view class="popup-content" @click.stop>
<view class="popup-header">
<text class="popup-title">快速获取户号</text>
<text class="popup-close" @click="showPopup = false">×</text>
</view>
<view class="popup-list">
<view class="popup-item">
<view class="item-number">1</view>
<view class="item-content">
<text class="item-title">查看缴费通知单</text>
<text class="item-desc">在纸质缴费/催费通知单上查找户号。</text>
</view>
</view>
<view class="popup-item">
<view class="item-number">2</view>
<view class="item-content">
<text class="item-title">查询通知短信</text>
<text class="item-desc">搜索手机由缴费单位发送的催缴短信,短信中包含户号</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部协议和立即缴费按钮 -->
<view class="bottom-area">
<view class="agreement">
<checkbox-group @change="handleAgreeChange">
<checkbox :checked="isAgree" />
</checkbox-group>
<text class="agreement-text">我已阅并同意</text>
<text class="agreement-link" @click="handleProtocol">《协议链接》</text>
</view>
<button class="submit-btn" @click="handleSubmit">
立即缴费
</button>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
// 响应式数据
const statusBarHeight = ref(20)
const paymentType = ref(uni.getStorageSync('addPaymentType'))
const userNo = ref('')
const isAgree = ref(false)
const showPopup = ref(false)
// 生命周期
onShow(() => {})
onMounted(() => {})
onReady(() => {
// nextTick 用法
// nextTick(() => {})
})
// 勾选协议
const handleAgreeChange = (e) => {
isAgree.value = e.detail.value.length > 0
}
// 协议链接点击
const handleProtocol = () => {
uni.navigateTo({})
}
// 提交
const handleSubmit = () => {
if (!userNo.value) {
uni.showToast({
title: '请输入户号',
icon: 'none'
})
return
}
if (!isAgree.value) {
uni.showToast({
title: '请先同意协议',
icon: 'none'
})
return
}
uni.showModal({
title: '确认提交',
content: '确认要提交吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '提交中...',
mask: true
})
setTimeout(() => {
uni.showToast({
title: '提交成功',
icon: 'success'
})
}, 1000)
setTimeout(() => {
uni.hideLoading()
goNext()
}, 2500)
}
}
})
}
// 跳转成功页
const goNext = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/addPaymentSuccess'
})
}
// 切换(注释保留)
const goToChange = () => {
// uni.redirectTo({
// url: '/pageSubPack/payment-list/selectPaymentEntity'
// })
}
// 返回
const goBack = () => {
uni.switchTab({ url: '/pages/paymentIndex/paymentIndex' });
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
.container {
border-radius: 10rpx;
}
/* 顶部标题栏 */
.header-card {
background: #fff;
padding: 30rpx;
display: flex;
align-items: center;
border-bottom: 2rpx solid #ededed;
}
.header-icon {
width: 56rpx;
height: 56rpx;
background-color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.iconStyle {
width: 100%;
height: 100%;
}
.icon-fire {
font-size: 32rpx;
color: #4080ff;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
}
/* 信息卡片 */
.info-card {
background-color: #fff;
border-radius: 10rpx;
/* padding: 30rpx; */
margin-bottom: 40rpx;
}
.form-item {
/* margin-bottom: 40rpx; */
border-bottom: 2rpx solid #ededed;
padding: 30rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.label {
display: block;
font-size: 28rpx;
color: #222;
margin-bottom: 20rpx;
}
.company-name {
display: block;
font-size: 32rpx;
color: #333;
font-weight: 600;
}
.input-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
}
.input-field {
flex: 1;
font-size: 32rpx;
color: #333;
padding: 10rpx 0;
border: none;
outline: none;
}
.input-field::placeholder {
color: #ccc;
}
.help-link {
font-size: 28rpx;
color: #4080ff;
white-space: nowrap;
margin-left: 20rpx;
}
/* 底部区域 */
.bottom-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 30rpx;
box-sizing: border-box;
}
.agreement {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.agreement checkbox {
transform: scale(0.8);
margin-right: 10rpx;
}
.agreement-text {
font-size: 24rpx;
color: #333;
}
.agreement-link {
font-size: 28rpx;
color: #4080ff;
margin-left: 10rpx;
}
.submit-btn {
width: 100%;
height: 90rpx;
line-height: 90rpx;
background-color: #4080ff;
color: #fff;
border-radius: 10rpx;
font-size: 36rpx;
font-weight: 500;
border: none;
outline: none;
}
.submit-btn[disabled] {
background-color: #a0c4ff;
color: #fff;
opacity: 0.7;
}
/* 弹窗遮罩层 */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
}
/* 弹窗内容 */
.popup-content {
width: 100%;
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx 40rpx 60rpx;
box-sizing: border-box;
animation: slideUp 0.3s ease-out;
}
/* 弹窗标题栏 */
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40rpx;
}
.popup-title {
font-size: 32rpx;
line-height: 60rpx;
font-weight: bold;
color: #333;
width: 100%;
display: flex;
/* align-items: center; */
justify-content: space-evenly;
border-bottom: 2rpx solid #ebebeb;
}
.popup-close {
font-size: 48rpx;
color: #999;
line-height: 1;
}
/* 弹窗列表 */
.popup-list {
padding: 0 10rpx;
}
.popup-item {
display: flex;
align-items: flex-start;
margin-bottom: 40rpx;
}
.popup-item:last-child {
margin-bottom: 0;
}
.item-number {
width: 40rpx;
height: 40rpx;
line-height: 40rpx;
text-align: center;
border: 2rpx solid #4080ff;
border-radius: 6rpx;
font-size: 28rpx;
font-weight: bold;
color: #4080ff;
margin-right: 20rpx;
flex-shrink: 0;
}
.item-content {
flex: 1;
display: flex;
flex-direction: column;
}
.item-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.item-desc {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
/* 弹窗动画 */
/* @keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
} */
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 9999;
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40rpx 60rpx;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.toast-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 50%;
background-color: #fff;
color: #1677ff;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.toast-text {
color: #fff;
font-size: 28rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="'新增'+paymentType+'缴费'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 成功图标 -->
<view class="success-icon">
<text class="icon-check">✓</text>
</view>
<!-- 主标题 -->
<view class="title">绑定成功</view>
<!-- 提示文案 -->
<view class="desc">
恭喜您绑定成功,费用将在24小时内更新,<br>
请您按时查看,避免造成费用欠费
</view>
<!-- 操作按钮 -->
<view class="btn-wrap">
<button class="btn-back" @click="goBack">返回生活缴费</button>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
// 响应式数据
const statusBarHeight = ref(20)
const paymentType = ref(uni.getStorageSync('addPaymentType'))
// 生命周期
onShow(() => {})
onMounted(() => {})
onReady(() => {})
// 方法
const goBack = () => {
uni.switchTab({ url: '/pages/paymentIndex/paymentIndex' });
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
</style>
<style scoped>
/* 全局容器 */
.container {
display: flex;
flex-direction: column;
align-items: center;
}
/* 成功图标 */
.success-icon {
width: 220rpx;
height: 220rpx;
border-radius: 50%;
background-color: #1677ff;
display: flex;
align-items: center;
justify-content: center;
margin-top: 200rpx;
margin-bottom: 30rpx;
}
.icon-check {
color: #fff;
font-size: 130rpx;
font-weight: bold;
line-height: 1;
}
/* 主标题 */
.title {
font-size: 44rpx;
font-weight: bold;
color: #000;
margin-bottom: 40rpx;
}
/* 提示文案 */
.desc {
font-size: 32rpx;
color: #666;
line-height: 1.6;
text-align: center;
margin-bottom: 150rpx;
}
/* 按钮容器 */
.btn-wrap {
width: 100%;
}
/* 返回按钮 */
.btn-back {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background-color: #1677ff;
color: #fff;
font-size: 36rpx;
border-radius: 8rpx;
border: none;
padding: 0;
margin: 0;
}
/* 按钮默认样式重置(解决uni-app button默认样式问题) */
.btn-back::after {
border: none;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,790 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="paymentItem.type || '缴费'" left-icon="left" @clickLeft="goBack"
backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 1. 缴费详情 -->
<view v-if="currentStep === 1" class="pay-page">
<!-- 物业费缴费 -->
<!-- 金额展示 -->
<view class="amount-section" v-if="outstandingPayment">
<text class="amount-text"
:class="{ 'outstandingPaymentClass':outstandingPayment }">{{outstandingPayment?'-¥'+payAmount:'¥'+payAmount}}</text>
</view>
<view class="amount-section" v-else>
<text class="amount-text paymentClass">{{'¥'+payAmount}}</text>
</view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<!-- 缴费信息 -->
<view class="info-section" style="">
<view>
<view class="info-item">
<text class="info-label">户号</text>
<text class="info-value">{{ paymentItem.deviceCode || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">{{ paymentItem.name || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">地址</text>
<text class="info-value">{{ paymentItem.fullAddress || '-' }}</text>
</view>
<!-- <view style="border-bottom: 2rpx solid #c7c7c7;padding-bottom:20rpx;"></view> -->
<!-- 缴费金额输入 -->
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<view class="input-section" style="display: flex;">
<text class="amount-input" style="width: auto;margin: 0rpx 20rpx;">¥</text>
<input type="digit" v-model="payNum" class="amount-input" placeholder="点击输入缴费金额">
</input>
</view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<view class="arrears-row" v-if="outstandingPayment==true">
<text class="arrears-text">当前欠费金额¥{{ arrearsAmount }}元</text>
<text class="auto-fill-btn" @click="handleAutoFill">点击自动填入</text>
</view>
<view class="arrears-row" v-if="outstandingPayment==false">
<text class="arrears-text">暂未查询到欠费</text>
</view>
</view>
</view>
</view>
<!-- 2. 缴费成功页 -->
<view class="container" v-if="currentStep==2">
<!-- 成功图标 -->
<view class="success-icon">
<text class="icon-check">✓</text>
</view>
<!-- 主标题 -->
<view class="title">缴费成功</view>
<!-- 提示文案 -->
<view class="desc">
成功缴纳{{ paymentItem.type || '' }}费用 ¥{{ payNum || payAmount }}元
</view>
</view>
</view>
</scroll-view>
<!-- 立即缴费按钮 -->
<view class="btn-section" v-if="currentStep === 1">
<button class="pay-btn" @click="handlePay">立即缴费</button>
</view>
<!-- 返回按钮 -->
<view class="btn-section" v-if="currentStep === 2">
<button class="pay-btn" @click="handleBackToList">返回生活缴费</button>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getTopUp,
getToWxPay,
getopenIdByCode,
postSubscribeAuth
} from '/pageSubPack/api/apiSub.js'
// 响应式数据
const statusBarHeight = ref(20)
const outstandingPayment = ref(true)
const storageValue = uni.getStorageSync('arrearsPayment')
const paymentItem = reactive(typeof storageValue === 'object' && storageValue !== null ? storageValue : {})
const currentStep = ref(1)
const payAmount = ref('0.00')
const arrearsAmount = ref('0.00')
const payNum = ref('')
// 页面加载时初始化数据
onLoad(() => {
initPaymentData()
checkAllSubscribeStatus()
})
// 初始化缴费数据
const initPaymentData = () => {
if (paymentItem) {
arrearsAmount.value = Math.abs(paymentItem.balance || 0).toFixed(2)
payAmount.value = Math.abs(paymentItem.balance || 0)
outstandingPayment.value = paymentItem.balance < 0
}
}
// 自动填充欠费金额
const handleAutoFill = () => {
payNum.value = arrearsAmount.value
uni.vibrateShort({
type: 'light'
})
}
// 返回
const goBack = () => {
uni.switchTab({
url: '/pages/paymentIndex/paymentIndex'
});
}
// 立即缴费
const handlePay = () => {
if (!payNum.value || Number(payNum.value) <= 0) {
uni.showToast({
title: '请输入正确的缴费金额',
icon: 'none'
})
return
}
requestPayOrder()
}
// 统一下单后后端返回的支付参数
const payInfo = ref({
appId: '', // 公众号/小程序ID
timeStamp: '', // 时间戳
nonceStr: '', // 随机串
package: '', // 数据包 如 prepay_id=xxx
signType: 'MD5', // 签名方式
paySign: '' // 签名
})
// 1. 先请求后端接口生成预支付订单
const requestPayOrder = async () => {
try {
uni.showLoading({
title: '发起支付中'
})
// // 获取 openid
// const loginRes = await wx.login()
// if (!loginRes.code) {
// uni.hideLoading()
// uni.showToast({
// title: '登录失败',
// icon: 'none'
// })
// return
// }
// // 用 code 换 openid
// const openIdRes = await getopenIdByCode({
// code: loginRes.code
// })
// if (openIdRes.code !== 200 || !openIdRes.data) {
// uni.hideLoading()
// uni.showToast({
// title: '获取openid失败',
// icon: 'none'
// })
// return
// }
// const openid = openIdRes.data
const openid = uni.getStorageSync('openid')
// 调用充值接口创建订单
console.log('1111111111', paymentItem);
const topUpRes = await getTopUp({
residentUserId: uni.getStorageSync('userId'),
rechargeAmount: payNum.value,
money: payNum.value || payAmount.value,
deviceCode: paymentItem.deviceCode,
// type: paymentItem.type == '水表' ? '2' : paymentItem.type == '电表' ? '1' : ''
type:'2'
})
uni.hideLoading()
const topUpData = topUpRes
console.log(topUpData);
if (topUpData.code !== 200) {
uni.showToast({
title: topUpData.msg || '下单失败',
icon: 'none'
})
return
}
// 调用微信支付接口获取支付参数
const payRes = await getToWxPay({
orderId: topUpData.data.orderId,
openid: openid,
// rechargeAmount: payNum.value || payAmount.value,
// type: paymentItem.type == '水表' ? '2' : paymentItem.type == '电表' ? '1' : ''
})
if (payRes.code !== 200) {
uni.showToast({
title: payRes.msg || '获取支付参数失败',
icon: 'none'
})
return
}
const payData = payRes.data
console.log('payData', payData);
// 赋值支付参数
payInfo.value = {
appId: payData.appId,
timeStamp: payData.timeStamp,
nonceStr: payData.nonceStr,
package: payData.package.startsWith('prepay_id=') ? payData.package : 'prepay_id=' + payData
.package,
signType: payData.signType,
paySign: payData.paySign
}
// 2. 调起微信支付
wx.requestPayment({
...payInfo.value,
success(res) {
console.log('支付成功', res)
currentStep.value = 2
},
fail(err) {
console.log('支付失败', err)
uni.showToast({
title: '支付取消或失败',
icon: 'none'
})
}
})
handleWaterElecSubscribe()
} catch (err) {
uni.hideLoading()
console.log('请求异常', err)
showApiError(err)
}
}
// 【双模板ID】你提供的 水表+电表 催缴模板
const TEMPLATE_IDS = {
water: 'DowYnoGKNtl_eYycRCq-nt7DAoFjH7aVcGUQciZl1v0',
elec: 'PE7Neemv0z77JnBIPmb-Qg2Q3pJSUDGaILCryFCxmaw'
}
// 转成数组(微信API要求)
const TEMPLATE_ID_ARRAY = Object.values(TEMPLATE_IDS)
// 1. 检测 水表+电表 订阅状态
const checkAllSubscribeStatus = () => {
uni.getSetting({
withSubscriptions: true,
success: (res) => {
const itemSettings = res.subscriptionsSetting?.itemSettings || {}
console.log('水表订阅状态:', itemSettings[TEMPLATE_IDS.water])
console.log('电表订阅状态:', itemSettings[TEMPLATE_IDS.elec])
}
})
}
// 2. 点击开启订阅(主方法)
const handleWaterElecSubscribe = () => {
uni.getSetting({
withSubscriptions: true,
success: (res) => {
const itemSettings = res.subscriptionsSetting?.itemSettings || {}
const waterStatus = itemSettings[TEMPLATE_IDS.water]
const elecStatus = itemSettings[TEMPLATE_IDS.elec]
// 如果任意一个被永久拒绝 → 引导去设置
if ((waterStatus === 'reject' || waterStatus === 'ban') ||
(elecStatus === 'reject' || elecStatus === 'ban')) {
uni.showModal({
title: '开启消息通知',
content: '请前往设置页打开水表/电表提醒开关',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) uni.openSetting()
}
})
return
}
// 正常弹出授权框
requestMultiSubscribe()
}
})
}
// 3. 调用微信授权(根据类型传对应模板)
const requestMultiSubscribe = () => {
const tmplIds = paymentItem.type == '水表' ? [TEMPLATE_IDS.water] : paymentItem.type == '电表' ? [TEMPLATE_IDS.elec] : TEMPLATE_ID_ARRAY
uni.requestSubscribeMessage({
tmplIds: tmplIds,
success: async (res) => {
console.log('双模板授权结果:', res)
let successCount = 0
// 水表授权成功
if (res[TEMPLATE_IDS.water] === 'accept') successCount++
// 电表授权成功
if (res[TEMPLATE_IDS.elec] === 'accept') successCount++
if (successCount > 0) {
uni.showToast({
title: `已开启${successCount}项提醒`,
icon: 'success'
})
// 保存用户openid到后端
await saveUserSubscribe()
} else {
uni.showToast({
title: '已取消授权',
icon: 'none'
})
}
},
fail: () => {
uni.showToast({
title: '授权失败',
icon: 'none'
})
}
})
}
// 4. 传给后端:保存订阅信息
const saveUserSubscribe = async () => {
const res = await postSubscribeAuth({
openid: uni.getStorageSync('openid'),
templateIds: paymentItem.type == '水表' ? [TEMPLATE_IDS.water] : paymentItem.type == '电表' ? [TEMPLATE_IDS.elec] : TEMPLATE_ID_ARRAY,
// deviceCode: paymentItem.deviceCode,
// deviceType: paymentItem.type == '水表' ? '2' : paymentItem.type == '电表' ? '1' : ''
})
if (res.data == 200) {
console.log('111111111111111:', res)
} else {
uni.showToast({
title: `${res.msg}`,
icon: 'none'
})
}
}
// 返回缴费列表
const handleBackToList = () => {
uni.switchTab({
url: '/pages/paymentIndex/paymentIndex'
});
}
</script>
<style lang="scss">
page {
background-color: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
background-color: #
/* fff */
;
}
/* 1. 物业费详情页 */
.pay-page {
width: 100%;
/* min-height: 100vh; */
/* background-color: #fff; */
}
.amount-section {
padding: 60rpx 0;
text-align: center;
}
.amount-text {
font-size: 56rpx;
font-weight: 600;
}
.outstandingPaymentClass {
color: #E34D59;
}
.paymentClass {
color: #00aaff;
}
.info-section {
padding-top: 20rpx;
/* margin-bottom: 40rpx; */
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
height: 80rpx;
}
.info-label {
font-size: 30rpx;
color: #666;
}
.info-value {
font-size: 30rpx;
color: #333;
font-weight: 600;
}
.info-payAmountItem {
height: 100rpx;
line-height: 100rpx;
margin-bottom: 60rpx;
}
.info-payAmount {
font-size: 50rpx;
color: #333;
font-weight: 600;
}
.input-section {
/* padding: 030rpx; */
}
.amount-input {
width: 100%;
height: 100rpx;
font-size: 32rpx;
line-height: 100rpx;
border: none;
outline: none;
margin-right: 10rpx;
}
.btn-section {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
}
.pay-btn {
width: 100%;
height: 100rpx;
line-height: 100rpx;
background-color: #007aff;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
border: none;
outline: none;
}
/* 2. 支付密码弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
}
.password-modal {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: 40rpx;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 100rpx;
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.close-icon {
font-size: 44rpx;
width: 80rpx;
height: 100rpx;
display: flex;
align-items: center;
}
.modal-title {
font-size: 32rpx;
font-weight: 500;
color: #000;
}
.header-right {
width: 80rpx;
}
.pay-info {
padding: 40rpx 30rpx;
text-align: center;
}
.merchant-name {
display: block;
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
.pay-amount {
font-size: 64rpx;
font-weight: 500;
color: #000;
}
.pay-way {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.way-label {
font-size: 30rpx;
color: #666;
}
.way-content {
display: flex;
align-items: center;
}
.bank-icon {
width: 32rpx;
height: 32rpx;
margin-right: 6rpx;
}
.bank-name {
font-size: 30rpx;
color: #333;
margin-right: 16rpx;
}
.arrow-icon {
font-size: 28rpx;
color: #999;
}
/* 银行卡列表 */
.bank-list {
padding: 030rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.bank-item {
display: flex;
align-items: center;
height: 100rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.bank-item:last-child {
border-bottom: none;
}
.bank-item-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.bank-item-name {
flex: 1;
font-size: 30rpx;
color: #333;
}
.check-icon {
font-size: 36rpx;
color: #007aff;
}
/* 密码输入框 */
.password-input {
display: flex;
justify-content: space-between;
padding: 40rpx 30rpx;
}
.password-item {
width: 100rpx;
height: 100rpx;
/* border: 2rpx solid #dcdcdc; */
background-color: #efefef;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
}
.password-dot {
width: 24rpx;
height: 24rpx;
background-color: #000;
border-radius: 50%;
}
/* 数字键盘 */
.keyboard {
width: 100%;
}
.keyboard-row {
display: flex;
}
.keyboard-item {
flex: 1;
height: 54px;
line-height: 54px;
text-align: center;
font-size: 48rpx;
color: #000;
border: 2rpx solid #f0f0f0;
background-color: #fff;
}
.keyboard-item:active {
background-color: #e5e5e5;
}
.keyboard-item.empty {
background-color: #f5f5f5;
border: none;
}
.keyboard-item.delete {
background-color: #f5f5f5;
border: none;
}
/* 3. 缴费成功页 */
/* 全局容器 */
.container {
display: flex;
flex-direction: column;
align-items: center;
}
/* 成功图标 */
.success-icon {
width: 220rpx;
height: 220rpx;
border-radius: 50%;
background-color: #1677ff;
display: flex;
align-items: center;
justify-content: center;
margin-top: 200rpx;
margin-bottom: 60rpx;
}
.icon-check {
color: #fff;
font-size: 130rpx;
font-weight: bold;
line-height: 1;
}
/* 主标题 */
.title {
font-size: 40rpx;
font-weight: bold;
color: #000;
margin-bottom: 20rpx;
}
/* 提示文案 */
.desc {
font-size: 32rpx;
color: #666;
line-height: 1.6;
text-align: center;
margin-bottom: 150rpx;
}
/* 核心:欠费金额行样式 */
.arrears-row {
display: flex;
align-items: center;
justify-content: space-between;
/* padding:30rpx; */
margin-bottom: 60rpx;
}
.arrears-text {
font-size: 28rpx;
color: #999;
}
.auto-fill-btn {
font-size: 28rpx;
color: #007aff;
/* 蓝色,匹配设计图 */
font-weight: 500;
padding: 10rpx 0;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,482 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="arrearsPayment" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 1. 物业费详情页 -->
<view class="pay-page">
<!-- 未欠费 -->
<!-- v-if="arrearsPayment=='暖气费'" -->
<view class="no-arrears-section">
<image src="https://wuyeadmin.bugtc.com/images/propertyImgs/noArrears.png"
class="background-image-style"></image>
<text class="noArrearsText">暂未查到欠费</text>
<view class="recordText">
<text>{{'最近充值:'+paymentItem.LastRechargeTime || '-'}}</text>
<text class="lineStyle">{{'|'}}</text>
<text>{{'充值金额:'+paymentItem.LastRechargeAmount+'元' || '-'}}</text>
</view>
</view>
<!-- <view>
<view class="amount-section">
<text class="amount-text">{{'¥'+ Math.abs(paymentItem.balance || 0).toFixed(2)}}</text>
</view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
</view> -->
<!-- 缴费信息 -->
<view class="info-section" style="">
<view v-if="arrearsPayment=='物业费'">
<view class="info-item">
<text class="info-label">户名</text>
<text
class="info-value">{{paymentItem.villageName+ paymentItem.fullAddress || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">{{ paymentItem.name || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">缴费单位</text>
<text class="info-value">花开物业</text>
</view>
<view class="info-item">
<text class="info-label">可用余额</text>
<text class="info-value">{{ Math.abs(paymentItem.balance || 0).toFixed(2) }}元</text>
</view>
</view>
<view v-else>
<view class="info-item">
<text class="info-label">房屋</text>
<text class="info-value">{{ paymentItem.fullAddress || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户号</text>
<text class="info-value">{{ paymentItem.deviceCode || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">{{ paymentItem.name || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">缴费单位</text>
<text class="info-value">花开物业</text>
</view>
<view class="info-item">
<text class="info-label">可用余额</text>
<text class="info-value">{{ Math.abs(paymentItem.balance || 0).toFixed(2) }}元</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
// 响应式数据
const statusBarHeight = ref(20)
const paymentItem = reactive(uni.getStorageSync('arrearsPayment') || {})
const arrearsPayment = computed(() => paymentItem.type || '缴费')
// 生命周期
onShow(() => {})
onMounted(() => {})
onReady(() => {})
// 方法
const goBack = () => {
uni.switchTab({
url: '/pages/paymentIndex/paymentIndex'
});
}
// 以下为原代码保留的方法(未使用但保持功能完整)
const handlePay = () => {}
const closePasswordModal = () => {}
const selectBank = () => {}
const inputPassword = () => {}
const deletePassword = () => {}
const handleConfirmPay = () => {}
const handleBackToList = () => {}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
.pay-page {
width: 100%;
}
.amount-section {
padding: 60rpx 0;
text-align: center;
}
.no-arrears-section {
padding: 30rpx 0;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
color: #999999;
}
.amount-text {
font-size: 56rpx;
font-weight: 600;
color: #007aff;
}
.background-image-style {
width: 466rpx;
height: 250rpx;
margin-bottom: 140rpx;
}
.noArrearsText {
padding-bottom: 50rpx;
width: 100%;
border-bottom: 2rpx solid #ededed;
}
.recordText {
font-size: 24rpx;
color: #222;
margin-top: 20rpx;
font-weight: 600;
}
.lineStyle {
margin: 0rpx 30rpx;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
height: 80rpx;
}
.info-label {
font-size: 30rpx;
color: #666;
}
.info-value {
font-size: 30rpx;
color: #333;
}
.info-payAmountItem {
height: 100rpx;
line-height: 100rpx;
margin-bottom: 60rpx;
}
.info-payAmount {
font-size: 50rpx;
color: #333;
font-weight: 600;
}
.input-section {
padding: 0 30rpx;
margin-bottom: 60rpx;
}
.amount-input {
width: 100%;
height: 100rpx;
font-size: 48rpx;
border: none;
outline: none;
}
.btn-section {
padding: 0 30rpx;
}
.pay-btn {
width: 100%;
height: 100rpx;
line-height: 100rpx;
background-color: #007aff;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
border: none;
outline: none;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
}
.password-modal {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: 40rpx;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 100rpx;
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.close-icon {
font-size: 44rpx;
width: 80rpx;
height: 100rpx;
display: flex;
align-items: center;
}
.modal-title {
font-size: 32rpx;
font-weight: 500;
color: #000;
}
.header-right {
width: 80rpx;
}
.pay-info {
padding: 40rpx 30rpx;
text-align: center;
}
.merchant-name {
display: block;
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
.pay-amount {
font-size: 64rpx;
font-weight: 500;
color: #000;
}
.pay-way {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.way-label {
font-size: 30rpx;
color: #666;
}
.way-content {
display: flex;
align-items: center;
}
.bank-icon {
width: 32rpx;
height: 32rpx;
margin-right: 6rpx;
}
.bank-name {
font-size: 30rpx;
color: #333;
margin-right: 16rpx;
}
.arrow-icon {
font-size: 28rpx;
color: #999;
}
.bank-list {
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.bank-item {
display: flex;
align-items: center;
height: 100rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.bank-item:last-child {
border-bottom: none;
}
.bank-item-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.bank-item-name {
flex: 1;
font-size: 30rpx;
color: #333;
}
.check-icon {
font-size: 36rpx;
color: #007aff;
}
.password-input {
display: flex;
justify-content: space-between;
padding: 40rpx 30rpx;
}
.password-item {
width: 100rpx;
height: 100rpx;
background-color: #efefef;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
}
.password-dot {
width: 24rpx;
height: 24rpx;
background-color: #000;
border-radius: 50%;
}
.keyboard {
width: 100%;
}
.keyboard-row {
display: flex;
}
.keyboard-item {
flex: 1;
height: 54px;
line-height: 54px;
text-align: center;
font-size: 48rpx;
color: #000;
border: 2rpx solid #f0f0f0;
background-color: #fff;
}
.keyboard-item:active {
background-color: #e5e5e5;
}
.keyboard-item.empty {
background-color: #f5f5f5;
border: none;
}
.keyboard-item.delete {
background-color: #f5f5f5;
border: none;
}
.success-page {
width: 100%;
min-height: 100vh;
background-color: #fff;
text-align: center;
}
.icon-box {
display: flex;
justify-content: center;
width: 100vw;
height: 1100rpx;
padding: 60rpx 0px;
}
.success-icon {
margin-bottom: 40rpx;
width: 100px;
height: 100px;
font-size: 80px;
color: #007aff;
background-color: #007aff;
color: #fff;
border-radius: 50%;
}
.success-text {
font-size: 40rpx;
font-weight: 600;
color: #000;
margin-bottom: 20rpx;
}
.success-desc {
font-size: 30rpx;
color: #999;
margin-bottom: 100rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,397 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="缴费记录" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<!-- 累计缴费 -->
<view class="total-box">
<view class="top-box">
<view class="total-title">累计缴费</view>
<view class="total-price">¥{{ totalAmount }}</view>
</view>
<view class="chart-box">
<view v-if="chartData.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<!-- 循环6个月柱状图 -->
<view class="chart-item" v-for="(item,index) in chartData" :key="index">
<!-- 柱子顶部金额文字 -->
<view class="chart-label">{{ item.price }}元</view>
<!-- 柱状图柱子 -->
<view class="chart-bar" :style="{ height: barHeight(item.price) }"></view>
<!-- 底部月份文字 -->
<view class="chart-month">{{ item.month }}</view>
</view>
</view>
</view>
<!-- 缴费明细 -->
<view class="list-wrapper">
<!-- 按月份分组渲染 -->
<view v-if="monthGroups.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<view class="month-group" v-for="(item, idx) in monthGroups" :key="idx">
<view class="month-title">
{{ item.month }}月 缴费 ¥{{ item.total }}元
</view>
<view class="item-list">
<view class="bill-item" v-for="bill in item.list" :key="bill.id">
<view class="bill-icon">
<image class="iconStyle" src="https://wuyeadmin.bugtc.com/images/propertyImgs/water.png"
v-if="bill.name=='水表'"></image>
<image class="iconStyle" src="https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png"
v-if="bill.name=='电表'"></image>
</view>
<view class="bill-info">
<view class="name">{{ bill.name }}</view>
<view class="account">{{ bill.account }}</view>
</view>
<view class="bill-price">-{{ bill.amount }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getTopupRecordWithStat
} from '/pageSubPack/api/apiSub.js'
// 响应式数据
const statusBarHeight = ref(20)
const pageNum = ref(1)
const pageSize = ref(10)
const totalAmount = ref('0.00')
const chartData = ref([])
const maxPrice = ref(100)
const barMaxHeight = ref(300)
const billList = ref([])
// 图标映射
const iconMap = {
'水表': 'https://wuyeadmin.bugtc.com/images/propertyImgs/water.png',
'电表': 'https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png',
'物业费': 'https://wuyeadmin.bugtc.com/images/propertyImgs/property.png',
'暖气费': 'https://wuyeadmin.bugtc.com/images/propertyImgs/heating.png',
'燃气费': 'https://wuyeadmin.bugtc.com/images/propertyImgs/gas.png'
}
// 获取缴费记录
const getRecords = async () => {
try {
const res = await getTopupRecordWithStat({
residentUserId: uni.getStorageSync('userId'),
pageNum: pageNum.value,
pageSize: pageSize.value
})
if (res.code === 200) {
const records = res.data.records || []
// 转换记录格式
billList.value = records.map(item => ({
id: item.id,
month: item.rechargeTime && item.rechargeTime.split('-')[1] ? parseInt(item.rechargeTime.split('-')[1], 10) : 1,
name: item.type == 1 ? '电表' : item.type == 2 ? '水表' : '',
account: item.deviceCode || '-',
amount: Number(item.rechargeAmount) || 0,
icon: iconMap[item.type == 1 ? '电表' : item.type == 2 ? '水表' : ''] || 'https://wuyeadmin.bugtc.com/images/propertyImgs/water.png'
}))
// 累计缴费金额
const total = records.reduce((sum, item) => sum + (Number(item.rechargeAmount) || 0), 0)
totalAmount.value = total.toFixed(2)
// 更新图表数据(根据 records 计算最近6个月)
updateChartData()
}
} catch (err) {
console.error('获取缴费记录失败', err)
}
}
// 更新图表数据(根据 records 计算最近6个月)
const updateChartData = () => {
// 按月份汇总金额
const monthTotal = {}
billList.value.forEach(bill => {
const monthKey = bill.month
if (!monthTotal[monthKey]) {
monthTotal[monthKey] = 0
}
monthTotal[monthKey] += bill.amount
})
// 取最近6个月(当前月往前)
const now = new Date()
const currentMonth = now.getMonth() + 1 // 1-12
const currentYear = now.getFullYear()
const months = []
for (let i = 0; i < 6; i++) {
let month = currentMonth - i
let year = currentYear
if (month <= 0) {
month += 12
year -= 1
}
months.push({
month,
year
})
}
chartData.value = months.map((m, i) => ({
month: (m.year + '').slice(-2) + '年' + m.month + '月',
year: m.year,
sortMonth: m.year * 12 + m.month,
price: monthTotal[m.month] || 0,
index: i
})).sort((a, b) => a.sortMonth - b.sortMonth)
const prices = chartData.value.map(c => c.price)
maxPrice.value = Math.max(...prices, 100)
}
// 计算属性 —— 月份分组
const monthGroups = computed(() => {
let map = {}
billList.value.forEach(bill => {
if (!map[bill.month]) {
map[bill.month] = {
month: bill.month,
total: 0,
list: []
}
}
map[bill.month].list.push(bill)
map[bill.month].total += bill.amount
})
return Object.values(map).sort((a, b) => b.month - a.month)
})
// 生命周期
onShow(() => {})
onMounted(() => {})
onLoad(() => {
getRecords()
})
onReady(() => {})
// 方法
const barHeight = (price) => {
const height = (price / maxPrice.value) * barMaxHeight.value
return `${height}rpx`
}
const goBack = () => {
uni.switchTab({
url: '/pages/paymentIndex/paymentIndex'
});
}
</script>
<style lang="scss">
page {
// background: #f2f1f6;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
.total-box {
margin-bottom: 30rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.top-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.total-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.total-price {
font-size: 48rpx;
font-weight: bold;
margin-top: 10rpx;
margin-bottom: 20rpx;
}
.chart-canvas {
width: calc(100vw - 60rpx);
height: 30vh;
}
.list-wrapper {
padding: 10rpx 0rpx;
}
.month-group {
border-bottom: 2rpx solid #f2f2f2;
padding: 30rpx;
background-color: #fff;
margin-bottom: 30rpx;
border-radius: 10rpx;
}
.month-title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 30rpx;
}
.bill-item {
display: flex;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 30rpx;
border-bottom: 1px solid #ebebeb;
}
.bill-item:last-child {
border-bottom: none;
margin-bottom: 0rpx;
padding-bottom: 0rpx;
}
.bill-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #4080FF;
margin-right: 24rpx;
font-size: 36rpx;
}
.iconStyle {
width: 100%;
height: 100%;
}
.bill-info {
flex: 1;
}
.name {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.account {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.bill-price {
font-size: 32rpx;
color: #333;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
<style scoped>
.chart-box {
width: 100%;
height: 444rpx;
display: flex;
align-items: flex-end;
justify-content: space-around;
box-sizing: border-box;
}
.chart-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 120rpx;
font-size: 24rpx;
}
.chart-label {
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
.chart-bar {
width: 40rpx;
background: #3388ff;
border-radius: 40rpx 40rpx 0 0;
}
.chart-month {
font-size: 26rpx;
color: #666;
margin-top: 24rpx;
}
</style>

View File

@@ -0,0 +1,376 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="选择缴费单位" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<!-- 城市 + 搜索 -->
<view class="search-section">
<!-- 城市选择器 -->
<uni-data-picker placeholder="选择省市" popup-title="选择省市" :localdata="alladdress"
field="code as value, value as text" orderby="value asc" :step-searh="true" self-field="code"
parent-field="code" @change="onchange" v-model="cityValue" class="city-picker">
<view class="city-select">
<text class="city-text ellipsis">{{ selectedCity }}</text>
<text class="arrow-icon"></text>
</view>
</uni-data-picker>
<!-- 搜索框 -->
<view class="search-box">
<input class="search-input" v-model="searchKey" placeholder="请输入缴费单位名称"
placeholder-class="input-placeholder" @input="onSearch" />
<uni-icons type="search" size="20" color="#999" />
</view>
</view>
<view v-if="filterList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">{{ '暂无数据' }}</text>
</view>
<view v-for="item in filterList">
<view class="group-title">{{item.name}}</view>
<view class="item" v-for="itemChild in item.children" :key="itemChild.id"
@click="selectItem(itemChild)">
{{ itemChild.name }}
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
// 响应式数据
const selectedCity = ref('选择城市')
const cityValue = ref('')
const alladdress = ref([{
text: "北京市",
value: "110000000000",
children: [{
text: "市辖区",
value: "110100000000",
children: [{
text: "东城区",
value: "110101000000"
},
{
text: "西城区",
value: "110102000000"
},
{
text: "朝阳区",
value: "110105000000"
},
{
text: "丰台区",
value: "110106000000"
},
{
text: "石景山区",
value: "110107000000"
},
{
text: "海淀区",
value: "110108000000"
},
{
text: "门头沟区",
value: "110109000000"
},
{
text: "房山区",
value: "110111000000"
},
{
text: "通州区",
value: "110112000000"
},
{
text: "顺义区",
value: "110113000000"
},
{
text: "昌平区",
value: "110114000000"
},
{
text: "大兴区",
value: "110115000000"
},
{
text: "怀柔区",
value: "110116000000"
},
{
text: "平谷区",
value: "110117000000"
},
{
text: "密云区",
value: "110118000000"
},
{
text: "延庆区",
value: "110119000000"
}
]
}]
}])
const searchKey = ref('')
const statusBarHeight = ref(20)
const showPicker = ref(false)
const keyword = ref('')
const companyList = ref([{
id: 7,
name: '官方机构',
children: [{
id: 1,
name: "XX市燃气集团有限公司"
},
{
id: 2,
name: "XX市供水有限公司"
},
{
id: 3,
name: "XX供电服务中心"
},
{
id: 4,
name: "XX新能源服务公司"
},
{
id: 5,
name: "XX公共事业缴费中心"
},
{
id: 6,
name: "XX综合能源服务站"
}
]
},
{
id: 8,
name: '非智享日照提供的服务单位',
children: [{
id: 9,
name: "XX市燃气集团有限公司"
},
{
id: 10,
name: "XX能源集团"
}
]
}
])
// 计算属性:搜索过滤
const filterList = computed(() => {
if (!searchKey.value) return companyList.value
const key = searchKey.value.trim().toLowerCase()
return companyList.value.filter(group => {
const hasChild = group.children.some(child =>
child.name.toLowerCase().includes(key)
)
const groupMatch = group.name.toLowerCase().includes(key)
return hasChild || groupMatch
}).map(group => {
return {
...group,
children: group.children.filter(child =>
child.name.toLowerCase().includes(key)
)
}
})
})
// 生命周期
onShow(() => {})
onMounted(() => {})
onReady(() => {})
// 方法
const selectItem = (item) => {
console.log(item)
uni.redirectTo({
url: '/pageSubPack/payment-list/addPayment'
})
}
const onchange = (e) => {
console.log('选中text数组:', e.detail.value)
const arr = e.detail.value
let city = ''
if (arr[1].text === '市辖区') {
city = arr[0].text
} else {
city = arr[1].text
}
selectedCity.value = city
}
const onSearch = () => {}
const goBack = () => {
uni.switchTab({ url: '/pages/paymentIndex/paymentIndex' });
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
box-sizing: border-box;
background-color: #fff;
}
.search-section {
background-color: #fff;
padding: 20rpx;
display: flex;
align-items: center;
}
.city-picker {
display: block;
width: auto;
}
.city-select {
display: flex;
align-items: center;
padding: 8rpx 0;
cursor: pointer;
width: 80px;
height: 70rpx;
}
.ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.city-text {
font-size: 28rpx;
font-weight: 600;
color: #000000;
line-height: 1;
}
.arrow-icon {
width: 0;
height: 0;
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-top: 16rpx solid #333333;
margin-top: 4rpx;
}
.search-box {
display: flex;
width: 100%;
align-items: center;
background-color: #f3f4f6;
border-radius: 20rpx;
padding: 0 30rpx;
margin: 10rpx 0px 0px 10rpx;
height: 70rpx;
}
.search-input {
flex: 1;
height: 100%;
font-size: 30rpx;
border: none;
background: transparent;
}
.input-placeholder {
color: #999;
font-size: 30rpx;
}
.search-icon {
font-size: 36rpx;
color: #999;
}
.group-title {
padding: 20rpx 40rpx;
font-size: 28rpx;
color: #666;
background-color: #f3f3f3;
}
.item {
background: #fff;
padding: 20rpx 40rpx;
font-size: 28rpx;
border-bottom: 2rpx solid #f2f2f2;
}
.item:active {
background: #f2f2f2;
}
::v-deep .selected-item {
margin-left: 10rpx !important;
margin-right: 10rpx !important;
}
.status-bar {
width: 100vw;
height: 46px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
</style>

484
huakai-app/pages.json Normal file
View File

@@ -0,0 +1,484 @@
{
"pages": [{
"path": "pages/navigation/navigation",
"style": {
"navigationBarTitleText": "引导页",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path": "pages/paymentIndex/paymentIndex",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#F8F8F8"
}
},
{
"path": "pages/serviceIndex/serviceIndex",
"style": {
"navigationBarTitleText": "服务",
"navigationStyle": "custom"
}
},
{
"path": "pages/myIndex/myIndex",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
}
],
"subPackages": [{
"root": "pageSubPack/loginSub",
"pages": [{
"path": "register-new-user",
"style": {
"navigationBarTitleText": "注册新用户",
"navigationStyle": "custom"
}
},
{
"path": "pwd-login",
"style": {
"navigationBarTitleText": "账号密码登录",
"navigationStyle": "custom"
}
},
{
"path": "login",
"style": {
"navigationBarTitleText": "验证码登录",
"navigationStyle": "custom"
}
},
{
"path": "forget-pwd",
"style": {
"navigationBarTitleText": "忘记密码",
"navigationStyle": "custom"
}
},
{
"path": "change-pwd",
"style": {
"navigationBarTitleText": "修改密码",
"navigationStyle": "custom"
}
},
{
"path": "privacy",
"style": {
"navigationBarTitleText": "用户隐私政策"
// "navigationStyle": "custom"
}
}
]
},
{
"root": "pageSubPack/online-repair",
"pages": [{
"path": "onlineRepair",
"style": {
"navigationBarTitleText": "在线报修",
"navigationStyle": "custom"
}
},
{
"path": "repairSelect",
"style": {
"navigationBarTitleText": "选择维修项目"
}
},
{
"path": "publicRepair",
"style": {
"navigationBarTitleText": "填写维修信息"
}
},
{
"path": "personalRepair",
"style": {
"navigationBarTitleText": "个人报修"
}
},
{
"path": "selectHouse",
"style": {
"navigationBarTitleText": "选择房屋"
}
},
{
"path": "personalSelectPoject",
"style": {
"navigationBarTitleText": "个人维修项目"
}
},
{
"path": "repairDetail",
"style": {
"navigationBarTitleText": "报修详情"
}
}
]
},
{
"root": "pageSubPack/visitor",
"pages": [{
"path": "visitor-list",
"style": {
"navigationBarTitleText": "访客邀请",
"navigationStyle": "custom"
}
},
{
"path": "visitor-detail",
"style": {
"navigationBarTitleText": "访客详情"
}
},
{
"path": "qrcode-page",
"style": {
"navigationBarTitleText": "通行二维码"
}
},
{
"path": "visitor-add",
"style": {
"navigationBarTitleText": "新增访客"
}
}
]
},
{
"root": "pageSubPack/activity-list",
"pages": [{
"path": "activityList",
"style": {
"navigationBarTitleText": "活动列表",
"navigationStyle": "custom"
}
},
{
"path": "live-add",
"style": {
"navigationBarTitleText": "添加实况"
}
},
{
"path": "activityListDetails",
"style": {
"navigationBarTitleText": "报名详情",
"navigationStyle": "custom"
}
}
]
},
{
"root": "pageSubPack/notice-list",
"pages": [{
"path": "noticeList",
"style": {
"navigationBarTitleText": "通知公告",
"navigationStyle": "custom"
}
},
{
"path": "noticeListDetails",
"style": {
"navigationBarTitleText": "公告详情"
}
},
{
"path": "oneClickOpen",
"style": {
"navigationBarTitleText": "一键开门",
"navigationStyle": "custom"
}
},
{
"path": "selectHome",
"style": {
"navigationBarTitleText": "选择小区"
}
}
]
},
{
"root": "pageSubPack/my",
"pages": [{
"path": "settingIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "changePhone",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "changePwd",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "myHouseIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "houseDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "addHouse",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "selectResidentialCommunity",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "selectBuilding",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "myParkingSpaceIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "parkingSpaceDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "addParkingSpace",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "bindHouse",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "selectParingLot",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "bindCar",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "myCarIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "addCar",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "selectCarBrand",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "carDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}
]
}, {
"root": "pageSubPack/payment-list",
"pages": [{
"path": "addPayment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "addPaymentSuccess",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "arrearsPayment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "noArrearsPayment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "paymentRecord",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "selectPaymentEntity",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "addHydropower",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}
]
},
{
"root": "pageSubPack/service-list",
"pages": [{
"path": "complaintsSuggestionsIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "complaintsDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "addComplaints",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "questionnaireSurveyIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "questionnaireFillout",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "questionnaireDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "contactProperty",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}
]
}
],
"easycom": {
"autoscan": true,
"custom": {
"^u--(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue"
}
},
"tabBar": {
"color": "#333333",
"backgroundColor": "#fff",
"borderStyle": "black",
"selectedColor": "#666666",
"list": [{
"pagePath": "pages/paymentIndex/paymentIndex",
"iconPath": "/static/tabbar/home.png",
"selectedIconPath": "/static/tabbar/homeBlue.png",
"text": "首页"
},
{
"pagePath": "pages/serviceIndex/serviceIndex",
"iconPath": "/static/tabbar/service.png",
"selectedIconPath": "/static/tabbar/serviceBlue.png",
"text": "服务"
},
{
"pagePath": "pages/myIndex/myIndex",
"iconPath": "/static/tabbar/mine.png",
"selectedIconPath": "/static/tabbar/mineBlue.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

View File

@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

View File

@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

View File

@@ -0,0 +1,243 @@
<template>
<view class="guide-page">
<view class="status-bar"></view>
<!-- 引导内容滑动容器 -->
<swiper
class="guide-swiper"
:current="currentStep"
@change="handleSwiperChange"
:disable-scroll="false"
:indicator-dots="false"
:mode="mode"
>
<!-- 便捷服务 -->
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="https://wuyeadmin.bugtc.com/images/guide1.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">便捷服务</view>
<view class="guide-subtitle">报修、缴费、预约</view>
</view>
</view>
</swiper-item>
<!-- 高效管理 -->
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="https://wuyeadmin.bugtc.com/images/guide2.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">高效管理</view>
<view class="guide-subtitle">自动化流程和智能设备</view>
</view>
</view>
</swiper-item>
<!--安全保障 -->
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="https://wuyeadmin.bugtc.com/images/guide3.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">安全保障</view>
<view class="guide-subtitle">智能巡检和智慧访客</view>
</view>
</view>
</swiper-item>
</swiper>
<!-- 进度点 -->
<view class="progress-dots">
<view class="dot" :class="{ active: currentStep === 0 }"></view>
<view class="dot" :class="{ active: currentStep === 1 }"></view>
<view class="dot" :class="{ active: currentStep === 2 }"></view>
</view>
<!-- 操作按钮 -->
<view class="guide-btn-wrap">
<button
class="guide-btn"
:class="{ primary: currentStep === 2 }"
@click="handleBtnClick"
>
{{ currentStep === 2 ? '立即体验' : '下一步' }}
</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
// ========== 响应式数据 ==========
// 当前引导步骤(0/1/2)
const currentStep = ref(0);
const mode = ref("default")
// ========== 方法 ==========
// 滑动swiper切换步骤
const handleSwiperChange = (e) => {
currentStep.value = e.detail.current;
};
// 点击按钮操作
const handleBtnClick = () => {
// 前两步:点击下一步切换
if (currentStep.value < 2) {
currentStep.value++;
} else {
// 立即体验,直接进入首页(不强制登录 — 仅在需要身份的功能上才要求登录)
uni.setStorageSync('hasShowGuide', true);
uni.switchTab({
url: '/pages/paymentIndex/paymentIndex'
});
}
};
</script>
<style scoped>
/* 页面容器 */
.guide-page {
width: 100%;
height: 100vh;
background: linear-gradient(to bottom, #E2F0FF, #F6FAFF);
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
/* 状态栏 - 自适应设备状态栏高度 */
.status-bar {
height: var(--status-bar-height);
width: 100%;
flex-shrink: 0;
}
/* 滑动容器 */
.guide-swiper {
flex: 1;
width: 100%;
}
/* 单个引导项 - 全屏自适应 */
.guide-item {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 60rpx;
box-sizing: border-box;
}
/* 图片区域 - 自适应 */
.guide-img {
width: 80%;
max-width: 600rpx;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
}
.img-content {
width: 100%;
height: 100%;
max-height: 50vh;
}
/* 文字区域 */
.guide-text {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
/* 引导标题 */
.guide-title {
font-size: 56rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
/* 引导副标题 */
.guide-subtitle {
font-size: 28rpx;
color: #666;
}
/* 进度点容器 */
.progress-dots {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
flex-shrink: 0;
}
/* 单个进度点 */
.dot {
width: 16rpx;
height: 8rpx;
border-radius: 8rpx;
background-color: #ddd;
margin: 0 8rpx;
transition: all 0.3s ease;
}
/* 激活的进度点 */
.dot.active {
background-color: #007aff;
width: 28rpx;
}
/* 按钮容器 */
.guide-btn-wrap {
width: 80%;
max-width: 600rpx;
margin-bottom: 80rpx;
flex-shrink: 0;
}
/* 引导按钮 */
.guide-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
padding: 0;
margin: 0;
box-sizing: border-box;
}
/* 默认按钮样式(下一步) */
.guide-btn:not(.primary) {
background-color: #fff;
color: #007aff;
border: 2rpx solid #007aff;
}
/* 主按钮样式(立即体验) */
.guide-btn.primary {
background-color: #007aff;
color: #fff;
border: none;
}
/* 按钮禁用态(可选) */
.guide-btn:disabled {
background-color: #ccc;
color: #fff;
border: none;
}
</style>

View File

@@ -0,0 +1,426 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="生活缴费" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<view class="topbox">
<image class="titleImg" src="https://wuyeadmin.bugtc.com/images/propertyImgs/payTitle.png"></image>
<image class="payImg" src="https://wuyeadmin.bugtc.com/images/propertyImgs/payImg.png"></image>
</view>
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 未登录提示条 -->
<view class="login-tip" v-if="!isLogin" @click="handleLoginPrompt">
<image class="login-tip-icon" src="https://wuyeadmin.bugtc.com/images/login/phone.png"></image>
<text class="login-tip-text">登录后可查看缴费账单</text>
<text class="login-tip-link">去登录</text>
</view>
<!-- 账单列表(仅登录后显示) -->
<view class="bill-list" v-if="isLogin && paymentList.length > 0">
<view class="bill-item" @click="goToBill(item)" v-for="item,index in paymentList" :key="index">
<image class="billIcon" src="https://wuyeadmin.bugtc.com/images/propertyImgs/water.png"
v-if="item.type=='水费'"></image>
<image class="billIcon" src="https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png"
v-if="item.type=='电费'"></image>
<view class="bill-info">
<view class="bill-title">{{item.type}}
<view class="bill-status debt" v-if="item.balance<0">已欠费</view>
<text :class="item.balance<0 ? 'balance-debt' : 'balance-normal'">(余额:{{item.balance|| 0.00}}元)</text>
</view>
<view class="bill-desc">{{item.villageName+"-"+item.buildingName+'-'+formatAddress(item.fullAddress)}}</view>
</view>
<view class="arrow"></view>
</view>
</view>
<!-- 新增缴费区域 -->
<view class="add-section">
<view class="section-header">
<text class="section-title">新增缴费</text>
<text class="record-link" @click="goToRecord">缴费记录</text>
</view>
<view class="grid-list">
<view class="grid-item" v-for="(item, index) in addList" :key="index" @click="goToAdd(item)">
<view class="icon-box" :class="item.iconClass">
<image class="iconStyle" :src="item.icon"></image>
</view>
<text class="item-name">{{ item.name}}</text>
</view>
</view>
</view>
<view style="width: 100%;height: 30rpx;"></view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { getTopUpSelectByResident, getopenIdByCode, postInsertOpenId } from '/pageSubPack/api/apiSub.js'
import { isLoggedIn, requireAuth } from '/pageSubPack/api/auth.js'
const isLogin = ref(false)
const paymentList = ref([])
const addList = ref([
{
name: "水表",
type: "water",
icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/water.png",
iconClass: "water"
},
])
// ===== 刷新登录状态 =====
const refreshLoginState = () => {
isLogin.value = isLoggedIn();
if (!isLogin.value) {
// 未登录时清空账单数据
paymentList.value = [];
}
};
// ===== openid 相关(仅登录后) =====
const postOpenId = async () => {
if (!isLogin.value) return;
try {
const loginRes = await wx.login()
if (!loginRes.code) {
console.error('wx.login 失败')
return
}
const openIdRes = await getopenIdByCode({ code: loginRes.code })
if (openIdRes.code !== 200 || !openIdRes.data) {
console.error('获取openid失败')
return
}
uni.setStorageSync('openid', openIdRes.data)
await postInsertOpenId({
openid: openIdRes.data,
residentUserId: uni.getStorageSync('userId')
})
} catch (err) {
console.error('postOpenId 报错:', err)
}
};
// ===== 获取缴费列表 =====
const getPayList = async () => {
if (!isLogin.value) return;
try {
const res = await getTopUpSelectByResident({
residentUserId: uni.getStorageSync('userId')
})
paymentList.value = res.data || []
} catch (err) {
console.error('列表接口报错:', err)
}
};
// ===== 生命周期 =====
onLoad(() => {
refreshLoginState();
uni.hideTabBar({ animation: false });
if (isLogin.value) {
postOpenId();
getPayList();
}
});
onShow(() => {
refreshLoginState();
if (isLogin.value) {
getPayList();
}
});
// ===== 点击事件(统一用 isLogin 判断) =====
// 点击"去登录"提示条
const handleLoginPrompt = () => {
if (isLogin.value) return;
requireAuth({
title: '查看缴费账单',
onLoggedIn: () => {
refreshLoginState();
getPayList();
}
});
};
// 点击账单明细
const goToBill = (item) => {
if (!isLogin.value) {
requireAuth({
title: '查看缴费详情',
onLoggedIn: () => {
refreshLoginState();
goToBill(item);
}
});
return;
}
uni.setStorageSync('arrearsPayment', item)
uni.redirectTo({ url: '/pageSubPack/payment-list/arrearsPayment' })
};
// 点击缴费记录
const goToRecord = () => {
if (!isLogin.value) {
requireAuth({
title: '查看缴费记录',
onLoggedIn: () => {
refreshLoginState();
goToRecord();
}
});
return;
}
uni.redirectTo({ url: '/pageSubPack/payment-list/paymentRecord' })
};
// 点击新增缴费(水表/电表等)
const goToAdd = (type) => {
if (!isLogin.value) {
requireAuth({
title: '新增缴费',
onLoggedIn: () => {
refreshLoginState();
goToAdd(type);
}
});
return;
}
if (type.name == '燃气' || type.name == '暖气') {
uni.redirectTo({ url: '/pageSubPack/payment-list/selectPaymentEntity' })
} else if (type.name == '水表' || type.name == '电表') {
uni.redirectTo({ url: '/pageSubPack/payment-list/addHydropower' })
}
uni.setStorageSync('addPaymentType', type.name)
};
const formatAddress = (addr) => {
if (!addr) return ''
return addr.replace(/\d+[层楼]\s*/g, '').replace(/单元\s*/g, '-').replace(/\s+/g, '')
};
</script>
<style lang="scss">
page {
background: linear-gradient(180deg, #dfedfd, #F6FAFF);
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(180deg, #dfedfd, #F6FAFF);
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
.topbox {
height: 150rpx;
width: 100%;
position: relative;
}
.titleImg {
width: 262rpx;
height: 74rpx;
position: absolute;
left: 40rpx;
bottom: 20rpx;
}
.payImg {
width: 349.97rpx;
height: 313.57rpx;
position: absolute;
right: -80rpx;
bottom: -140rpx;
}
/* ===== 未登录提示条 ===== */
.login-tip {
display: flex;
align-items: center;
background-color: #fff;
padding: 24rpx 30rpx;
border-radius: 10rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
}
.login-tip-icon {
width: 40rpx;
height: 40rpx;
margin-right: 16rpx;
}
.login-tip-text {
flex: 1;
font-size: 28rpx;
color: #666;
}
.login-tip-link {
font-size: 28rpx;
color: #1677ff;
font-weight: 500;
}
.bill-list {
margin-bottom: 40rpx;
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
}
.bill-item {
display: flex;
align-items: center;
background-color: #F6F9FE;
border-radius: 12rpx;
padding: 20rpx 20rpx 20rpx 0px;
margin-bottom: 20rpx;
}
.bill-item:last-child {
margin-bottom: 0rpx;
}
.bill-info {
width: calc(90% - 100rpx);
flex: 1;
}
.billIcon {
width: 64rpx;
height: 64rpx;
margin: 20rpx;
}
.bill-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.bill-desc {
font-size: 28rpx;
font-weight: 600;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bill-status {
padding: 2rpx 16rpx;
border-radius: 30rpx;
font-size: 24rpx;
font-weight: 500;
color: #fff;
background-color: #E34D59;
}
.balance-normal {
font-size: 28rpx;
color: #333;
}
.balance-debt {
font-size: 28rpx;
color: #E34D59;
}
.arrow {
width: 12rpx;
height: 12rpx;
border-right: 4rpx solid #ccc;
border-bottom: 4rpx solid #ccc;
transform: rotate(-45deg);
}
.add-section {
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.record-link {
font-size: 28rpx;
color: #1677ff;
}
.grid-list {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 26rpx;
}
.grid-item {
width: calc(25% - 50rpx);
background-color: #F6F9FE;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
flex-direction: column;
}
.icon-box {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
}
.iconStyle {
width: 100%;
height: 100%;
}
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

View File

@@ -0,0 +1,13 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (!res) return resolve(res)
return res[0] ? reject(res[0]) : resolve(res[1])
});
});
},
});

77
huakai-app/uni.scss Normal file
View File

@@ -0,0 +1,77 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
@import '@/uni_modules/uview-plus/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:16px;
/* 图片尺寸 */
$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;

View File

@@ -0,0 +1,33 @@
## 1.2.2(2023-01-28)
- 修复 运行/打包 控制台警告问题
## 1.2.1(2022-09-05)
- 修复 当 text 超过 max-num 时,badge 的宽度计算是根据 text 的长度计算,更改为 css 计算实际展示宽度,详见:[https://ask.dcloud.net.cn/question/150473](https://ask.dcloud.net.cn/question/150473)
## 1.2.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-badge](https://uniapp.dcloud.io/component/uniui/uni-badge)
## 1.1.7(2021-11-08)
- 优化 升级ui
- 修改 size 属性默认值调整为 small
- 修改 type 属性,默认值调整为 error,info 替换 default
## 1.1.6(2021-09-22)
- 修复 在字节小程序上样式不生效的 bug
## 1.1.5(2021-07-30)
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.4(2021-07-29)
- 修复 去掉 nvue 不支持css 的 align-self 属性,nvue 下不暂支持 absolute 属性
## 1.1.3(2021-06-24)
- 优化 示例项目
## 1.1.1(2021-05-12)
- 新增 组件示例地址
## 1.1.0(2021-05-12)
- 新增 uni-badge 的 absolute 属性,支持定位
- 新增 uni-badge 的 offset 属性,支持定位偏移
- 新增 uni-badge 的 is-dot 属性,支持仅显示有一个小点
- 新增 uni-badge 的 max-num 属性,支持自定义封顶的数字值,超过 99 显示99+
- 优化 uni-badge 属性 custom-style, 支持以对象形式自定义样式
## 1.0.7(2021-05-07)
- 修复 uni-badge 在 App 端,数字小于10时不是圆形的bug
- 修复 uni-badge 在父元素不是 flex 布局时,宽度缩小的bug
- 新增 uni-badge 属性 custom-style, 支持自定义样式
## 1.0.6(2021-02-04)
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,268 @@
<template>
<view class="uni-badge--x">
<slot />
<text v-if="text" :class="classNames" :style="[positionStyle, customStyle, dotStyle]"
class="uni-badge" @click="onClick()">{{displayValue}}</text>
</view>
</template>
<script>
/**
* Badge 数字角标
* @description 数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景
* @tutorial https://ext.dcloud.net.cn/plugin?id=21
* @property {String} text 角标内容
* @property {String} size = [normal|small] 角标内容
* @property {String} type = [info|primary|success|warning|error] 颜色类型
* @value info 灰色
* @value primary 蓝色
* @value success 绿色
* @value warning 黄色
* @value error 红色
* @property {String} inverted = [true|false] 是否无需背景颜色
* @property {Number} maxNum 展示封顶的数字值,超过 99 显示 99+
* @property {String} absolute = [rightTop|rightBottom|leftBottom|leftTop] 开启绝对定位, 角标将定位到其包裹的标签的四角上
* @value rightTop 右上
* @value rightBottom 右下
* @value leftTop 左上
* @value leftBottom 左下
* @property {Array[number]} offset 距定位角中心点的偏移量,只有存在 absolute 属性时有效,例如:[-10, -10] 表示向外偏移 10px,[10, 10] 表示向 absolute 指定的内偏移 10px
* @property {String} isDot = [true|false] 是否显示为一个小点
* @event {Function} click 点击 Badge 触发事件
* @example <uni-badge text="1"></uni-badge>
*/
export default {
name: 'UniBadge',
emits: ['click'],
props: {
type: {
type: String,
default: 'error'
},
inverted: {
type: Boolean,
default: false
},
isDot: {
type: Boolean,
default: false
},
maxNum: {
type: Number,
default: 99
},
absolute: {
type: String,
default: ''
},
offset: {
type: Array,
default () {
return [0, 0]
}
},
text: {
type: [String, Number],
default: ''
},
size: {
type: String,
default: 'small'
},
customStyle: {
type: Object,
default () {
return {}
}
}
},
data() {
return {};
},
computed: {
width() {
return String(this.text).length * 8 + 12
},
classNames() {
const {
inverted,
type,
size,
absolute
} = this
return [
inverted ? 'uni-badge--' + type + '-inverted' : '',
'uni-badge--' + type,
'uni-badge--' + size,
absolute ? 'uni-badge--absolute' : ''
].join(' ')
},
positionStyle() {
if (!this.absolute) return {}
let w = this.width / 2,
h = 10
if (this.isDot) {
w = 5
h = 5
}
const x = `${- w + this.offset[0]}px`
const y = `${- h + this.offset[1]}px`
const whiteList = {
rightTop: {
right: x,
top: y
},
rightBottom: {
right: x,
bottom: y
},
leftBottom: {
left: x,
bottom: y
},
leftTop: {
left: x,
top: y
}
}
const match = whiteList[this.absolute]
return match ? match : whiteList['rightTop']
},
dotStyle() {
if (!this.isDot) return {}
return {
width: '10px',
minWidth: '0',
height: '10px',
padding: '0',
borderRadius: '10px'
}
},
displayValue() {
const {
isDot,
text,
maxNum
} = this
return isDot ? '' : (Number(text) > maxNum ? `${maxNum}+` : text)
}
},
methods: {
onClick() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" >
$uni-primary: #2979ff !default;
$uni-success: #4cd964 !default;
$uni-warning: #f0ad4e !default;
$uni-error: #dd524d !default;
$uni-info: #909399 !default;
$bage-size: 12px;
$bage-small: scale(0.8);
.uni-badge--x {
/* #ifdef APP-NVUE */
// align-self: flex-start;
/* #endif */
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
position: relative;
}
.uni-badge--absolute {
position: absolute;
}
.uni-badge--small {
transform: $bage-small;
transform-origin: center center;
}
.uni-badge {
/* #ifndef APP-NVUE */
display: flex;
overflow: hidden;
box-sizing: border-box;
font-feature-settings: "tnum";
min-width: 20px;
/* #endif */
justify-content: center;
flex-direction: row;
height: 20px;
padding: 0 4px;
line-height: 18px;
color: #fff;
border-radius: 100px;
background-color: $uni-info;
background-color: transparent;
border: 1px solid #fff;
text-align: center;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-size: $bage-size;
/* #ifdef H5 */
z-index: 999;
cursor: pointer;
/* #endif */
&--info {
color: #fff;
background-color: $uni-info;
}
&--primary {
background-color: $uni-primary;
}
&--success {
background-color: $uni-success;
}
&--warning {
background-color: $uni-warning;
}
&--error {
background-color: $uni-error;
}
&--inverted {
padding: 0 5px 0 0;
color: $uni-info;
}
&--info-inverted {
color: $uni-info;
background-color: transparent;
}
&--primary-inverted {
color: $uni-primary;
background-color: transparent;
}
&--success-inverted {
color: $uni-success;
background-color: transparent;
}
&--warning-inverted {
color: $uni-warning;
background-color: transparent;
}
&--error-inverted {
color: $uni-error;
background-color: transparent;
}
}
</style>

View File

@@ -0,0 +1,85 @@
{
"id": "uni-badge",
"displayName": "uni-badge 数字角标",
"version": "1.2.2",
"description": "数字角标(徽章)组件,在元素周围展示消息提醒,一般用于列表、九宫格、按钮等地方。",
"keywords": [
"",
"badge",
"uni-ui",
"uniui",
"数字角标",
"徽章"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": ["uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
## Badge 数字角标
> **组件名:uni-badge**
> 代码块: `uBadge`
数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景,
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-badge)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

View File

@@ -0,0 +1,30 @@
## 1.4.12(2024-09-21)
- 修复 calendar在选择日期范围后重新选择日期需要点两次的Bug
## 1.4.11(2024-01-10)
- 修复 回到今天时,月份显示不一致问题
## 1.4.10(2023-04-10)
- 修复 某些情况 monthSwitch 未触发的Bug
## 1.4.9(2023-02-02)
- 修复 某些情况切换月份错误的Bug
## 1.4.8(2023-01-30)
- 修复 某些情况切换月份错误的Bug [详情](https://ask.dcloud.net.cn/question/161964)
## 1.4.7(2022-09-16)
- 优化 支持使用 uni-scss 控制主题色
## 1.4.6(2022-09-08)
- 修复 表头年月切换,导致改变当前日期为选择月1号,且未触发change事件的Bug
## 1.4.5(2022-02-25)
- 修复 条件编译 nvue 不支持的 css 样式的Bug
## 1.4.4(2022-02-25)
- 修复 条件编译 nvue 不支持的 css 样式的Bug
## 1.4.3(2021-09-22)
- 修复 startDate、 endDate 属性失效的Bug
## 1.4.2(2021-08-24)
- 新增 支持国际化
## 1.4.1(2021-08-05)
- 修复 弹出层被 tabbar 遮盖的Bug
## 1.4.0(2021-07-30)
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.3.16(2021-05-12)
- 新增 组件示例地址
## 1.3.15(2021-02-04)
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,544 @@
/**
* @1900-2100区间内的公历、农历互转
* @charset UTF-8
* @github https://github.com/jjonline/calendar.js
* @Author Jea杨(JJonline@JJonline.Cn)
* @Time 2014-7-21
* @Time 2016-8-13 Fixed 2033hex、Attribution Annals
* @Time 2016-9-25 Fixed lunar LeapMonth Param Bug
* @Time 2017-7-24 Fixed use getTerm Func Param Error.use solar year,NOT lunar year
* @Version 1.0.3
* @公历转农历:calendar.solar2lunar(1987,11,01); //[you can ignore params of prefix 0]
* @农历转公历:calendar.lunar2solar(1987,09,10); //[you can ignore params of prefix 0]
*/
/* eslint-disable */
var calendar = {
/**
* 农历1900-2100的润大小信息表
* @Array Of Property
* @return Hex
*/
lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1900-1909
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, // 1910-1919
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, // 1920-1929
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, // 1930-1939
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, // 1940-1949
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, // 1950-1959
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, // 1960-1969
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, // 1970-1979
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, // 1980-1989
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0, // 1990-1999
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, // 2000-2009
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, // 2010-2019
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, // 2020-2029
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, // 2030-2039
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, // 2040-2049
/** Add By JJonline@JJonline.Cn**/
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, // 2050-2059
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, // 2060-2069
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, // 2070-2079
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, // 2080-2089
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, // 2090-2099
0x0d520], // 2100
/**
* 公历每个月份的天数普通表
* @Array Of Property
* @return Number
*/
solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
/**
* 天干地支之天干速查表
* @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"]
* @return Cn string
*/
Gan: ['\u7532', '\u4e59', '\u4e19', '\u4e01', '\u620a', '\u5df1', '\u5e9a', '\u8f9b', '\u58ec', '\u7678'],
/**
* 天干地支之地支速查表
* @Array Of Property
* @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"]
* @return Cn string
*/
Zhi: ['\u5b50', '\u4e11', '\u5bc5', '\u536f', '\u8fb0', '\u5df3', '\u5348', '\u672a', '\u7533', '\u9149', '\u620c', '\u4ea5'],
/**
* 天干地支之地支速查表<=>生肖
* @Array Of Property
* @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"]
* @return Cn string
*/
Animals: ['\u9f20', '\u725b', '\u864e', '\u5154', '\u9f99', '\u86c7', '\u9a6c', '\u7f8a', '\u7334', '\u9e21', '\u72d7', '\u732a'],
/**
* 24节气速查表
* @Array Of Property
* @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"]
* @return Cn string
*/
solarTerm: ['\u5c0f\u5bd2', '\u5927\u5bd2', '\u7acb\u6625', '\u96e8\u6c34', '\u60ca\u86f0', '\u6625\u5206', '\u6e05\u660e', '\u8c37\u96e8', '\u7acb\u590f', '\u5c0f\u6ee1', '\u8292\u79cd', '\u590f\u81f3', '\u5c0f\u6691', '\u5927\u6691', '\u7acb\u79cb', '\u5904\u6691', '\u767d\u9732', '\u79cb\u5206', '\u5bd2\u9732', '\u971c\u964d', '\u7acb\u51ac', '\u5c0f\u96ea', '\u5927\u96ea', '\u51ac\u81f3'],
/**
* 1900-2100各年的24节气日期速查表
* @Array Of Property
* @return 0x string For splice
*/
sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
'97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
'97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
'97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
'97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
'97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
'9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
'97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
'97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
'7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
'97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
'977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
'7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
'7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
'665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'],
/**
* 数字转中文速查表
* @Array Of Property
* @trans ['日','一','二','三','四','五','六','七','八','九','十']
* @return Cn string
*/
nStr1: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d', '\u4e03', '\u516b', '\u4e5d', '\u5341'],
/**
* 日期转农历称呼速查表
* @Array Of Property
* @trans ['初','十','廿','卅']
* @return Cn string
*/
nStr2: ['\u521d', '\u5341', '\u5eff', '\u5345'],
/**
* 月份转农历称呼速查表
* @Array Of Property
* @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊']
* @return Cn string
*/
nStr3: ['\u6b63', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d', '\u4e03', '\u516b', '\u4e5d', '\u5341', '\u51ac', '\u814a'],
/**
* 返回农历y年一整年的总天数
* @param lunar Year
* @return Number
* @eg:var count = calendar.lYearDays(1987) ;//count=387
*/
lYearDays: function (y) {
var i; var sum = 348
for (i = 0x8000; i > 0x8; i >>= 1) { sum += (this.lunarInfo[y - 1900] & i) ? 1 : 0 }
return (sum + this.leapDays(y))
},
/**
* 返回农历y年闰月是哪个月;若y年没有闰月 则返回0
* @param lunar Year
* @return Number (0-12)
* @eg:var leapMonth = calendar.leapMonth(1987) ;//leapMonth=6
*/
leapMonth: function (y) { // 闰字编码 \u95f0
return (this.lunarInfo[y - 1900] & 0xf)
},
/**
* 返回农历y年闰月的天数 若该年没有闰月则返回0
* @param lunar Year
* @return Number (0、29、30)
* @eg:var leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29
*/
leapDays: function (y) {
if (this.leapMonth(y)) {
return ((this.lunarInfo[y - 1900] & 0x10000) ? 30 : 29)
}
return (0)
},
/**
* 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法
* @param lunar Year
* @return Number (-1、29、30)
* @eg:var MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29
*/
monthDays: function (y, m) {
if (m > 12 || m < 1) { return -1 }// 月份参数从1至12,参数错误返回-1
return ((this.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29)
},
/**
* 返回公历(!)y年m月的天数
* @param solar Year
* @return Number (-1、28、29、30、31)
* @eg:var solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30
*/
solarDays: function (y, m) {
if (m > 12 || m < 1) { return -1 } // 若参数错误 返回-1
var ms = m - 1
if (ms == 1) { // 2月份的闰平规律测算后确认返回28或29
return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28)
} else {
return (this.solarMonth[ms])
}
},
/**
* 农历年份转换为干支纪年
* @param lYear 农历年的年份数
* @return Cn string
*/
toGanZhiYear: function (lYear) {
var ganKey = (lYear - 3) % 10
var zhiKey = (lYear - 3) % 12
if (ganKey == 0) ganKey = 10// 如果余数为0则为最后一个天干
if (zhiKey == 0) zhiKey = 12// 如果余数为0则为最后一个地支
return this.Gan[ganKey - 1] + this.Zhi[zhiKey - 1]
},
/**
* 公历月、日判断所属星座
* @param cMonth [description]
* @param cDay [description]
* @return Cn string
*/
toAstro: function (cMonth, cDay) {
var s = '\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf'
var arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22]
return s.substr(cMonth * 2 - (cDay < arr[cMonth - 1] ? 2 : 0), 2) + '\u5ea7'// 座
},
/**
* 传入offset偏移量返回干支
* @param offset 相对甲子的偏移量
* @return Cn string
*/
toGanZhi: function (offset) {
return this.Gan[offset % 10] + this.Zhi[offset % 12]
},
/**
* 传入公历(!)y年获得该年第n个节气的公历日期
* @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起
* @return day Number
* @eg:var _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春
*/
getTerm: function (y, n) {
if (y < 1900 || y > 2100) { return -1 }
if (n < 1 || n > 24) { return -1 }
var _table = this.sTermInfo[y - 1900]
var _info = [
parseInt('0x' + _table.substr(0, 5)).toString(),
parseInt('0x' + _table.substr(5, 5)).toString(),
parseInt('0x' + _table.substr(10, 5)).toString(),
parseInt('0x' + _table.substr(15, 5)).toString(),
parseInt('0x' + _table.substr(20, 5)).toString(),
parseInt('0x' + _table.substr(25, 5)).toString()
]
var _calday = [
_info[0].substr(0, 1),
_info[0].substr(1, 2),
_info[0].substr(3, 1),
_info[0].substr(4, 2),
_info[1].substr(0, 1),
_info[1].substr(1, 2),
_info[1].substr(3, 1),
_info[1].substr(4, 2),
_info[2].substr(0, 1),
_info[2].substr(1, 2),
_info[2].substr(3, 1),
_info[2].substr(4, 2),
_info[3].substr(0, 1),
_info[3].substr(1, 2),
_info[3].substr(3, 1),
_info[3].substr(4, 2),
_info[4].substr(0, 1),
_info[4].substr(1, 2),
_info[4].substr(3, 1),
_info[4].substr(4, 2),
_info[5].substr(0, 1),
_info[5].substr(1, 2),
_info[5].substr(3, 1),
_info[5].substr(4, 2)
]
return parseInt(_calday[n - 1])
},
/**
* 传入农历数字月份返回汉语通俗表示法
* @param lunar month
* @return Cn string
* @eg:var cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月'
*/
toChinaMonth: function (m) { // 月 => \u6708
if (m > 12 || m < 1) { return -1 } // 若参数错误 返回-1
var s = this.nStr3[m - 1]
s += '\u6708'// 加上月字
return s
},
/**
* 传入农历日期数字返回汉字表示法
* @param lunar day
* @return Cn string
* @eg:var cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一'
*/
toChinaDay: function (d) { // 日 => \u65e5
var s
switch (d) {
case 10:
s = '\u521d\u5341'; break
case 20:
s = '\u4e8c\u5341'; break
case 30:
s = '\u4e09\u5341'; break
default :
s = this.nStr2[Math.floor(d / 10)]
s += this.nStr1[d % 10]
}
return (s)
},
/**
* 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春”
* @param y year
* @return Cn string
* @eg:var animal = calendar.getAnimal(1987) ;//animal='兔'
*/
getAnimal: function (y) {
return this.Animals[(y - 4) % 12]
},
/**
* 传入阳历年月日获得详细的公历、农历object信息 <=>JSON
* @param y solar year
* @param m solar month
* @param d solar day
* @return JSON object
* @eg:console.log(calendar.solar2lunar(1987,11,01));
*/
solar2lunar: function (y, m, d) { // 参数区间1900.1.31~2100.12.31
// 年份限定、上限
if (y < 1900 || y > 2100) {
return -1// undefined转换为数字变为NaN
}
// 公历传参最下限
if (y == 1900 && m == 1 && d < 31) {
return -1
}
// 未传参 获得当天
if (!y) {
var objDate = new Date()
} else {
var objDate = new Date(y, parseInt(m) - 1, d)
}
var i; var leap = 0; var temp = 0
// 修正ymd参数
var y = objDate.getFullYear()
var m = objDate.getMonth() + 1
var d = objDate.getDate()
var offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) / 86400000
for (i = 1900; i < 2101 && offset > 0; i++) {
temp = this.lYearDays(i)
offset -= temp
}
if (offset < 0) {
offset += temp; i--
}
// 是否今天
var isTodayObj = new Date()
var isToday = false
if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) {
isToday = true
}
// 星期几
var nWeek = objDate.getDay()
var cWeek = this.nStr1[nWeek]
// 数字表示周几顺应天朝周一开始的惯例
if (nWeek == 0) {
nWeek = 7
}
// 农历年
var year = i
var leap = this.leapMonth(i) // 闰哪个月
var isLeap = false
// 效验闰月
for (i = 1; i < 13 && offset > 0; i++) {
// 闰月
if (leap > 0 && i == (leap + 1) && isLeap == false) {
--i
isLeap = true; temp = this.leapDays(year) // 计算农历闰月天数
} else {
temp = this.monthDays(year, i)// 计算农历普通月天数
}
// 解除闰月
if (isLeap == true && i == (leap + 1)) { isLeap = false }
offset -= temp
}
// 闰月导致数组下标重叠取反
if (offset == 0 && leap > 0 && i == leap + 1) {
if (isLeap) {
isLeap = false
} else {
isLeap = true; --i
}
}
if (offset < 0) {
offset += temp; --i
}
// 农历月
var month = i
// 农历日
var day = offset + 1
// 天干地支处理
var sm = m - 1
var gzY = this.toGanZhiYear(year)
// 当月的两个节气
// bugfix-2017-7-24 11:03:38 use lunar Year Param `y` Not `year`
var firstNode = this.getTerm(y, (m * 2 - 1))// 返回当月「节」为几日开始
var secondNode = this.getTerm(y, (m * 2))// 返回当月「节」为几日开始
// 依据12节气修正干支月
var gzM = this.toGanZhi((y - 1900) * 12 + m + 11)
if (d >= firstNode) {
gzM = this.toGanZhi((y - 1900) * 12 + m + 12)
}
// 传入的日期的节气与否
var isTerm = false
var Term = null
if (firstNode == d) {
isTerm = true
Term = this.solarTerm[m * 2 - 2]
}
if (secondNode == d) {
isTerm = true
Term = this.solarTerm[m * 2 - 1]
}
// 日柱 当月一日与 1900/1/1 相差天数
var dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10
var gzD = this.toGanZhi(dayCyclical + d - 1)
// 该日期所属的星座
var astro = this.toAstro(m, d)
return { 'lYear': year, 'lMonth': month, 'lDay': day, 'Animal': this.getAnimal(year), 'IMonthCn': (isLeap ? '\u95f0' : '') + this.toChinaMonth(month), 'IDayCn': this.toChinaDay(day), 'cYear': y, 'cMonth': m, 'cDay': d, 'gzYear': gzY, 'gzMonth': gzM, 'gzDay': gzD, 'isToday': isToday, 'isLeap': isLeap, 'nWeek': nWeek, 'ncWeek': '\u661f\u671f' + cWeek, 'isTerm': isTerm, 'Term': Term, 'astro': astro }
},
/**
* 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON
* @param y lunar year
* @param m lunar month
* @param d lunar day
* @param isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可]
* @return JSON object
* @eg:console.log(calendar.lunar2solar(1987,9,10));
*/
lunar2solar: function (y, m, d, isLeapMonth) { // 参数区间1900.1.31~2100.12.1
var isLeapMonth = !!isLeapMonth
var leapOffset = 0
var leapMonth = this.leapMonth(y)
var leapDay = this.leapDays(y)
if (isLeapMonth && (leapMonth != m)) { return -1 }// 传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同
if (y == 2100 && m == 12 && d > 1 || y == 1900 && m == 1 && d < 31) { return -1 }// 超出了最大极限值
var day = this.monthDays(y, m)
var _day = day
// bugFix 2016-9-25
// if month is leap, _day use leapDays method
if (isLeapMonth) {
_day = this.leapDays(y, m)
}
if (y < 1900 || y > 2100 || d > _day) { return -1 }// 参数合法性效验
// 计算农历的时间差
var offset = 0
for (var i = 1900; i < y; i++) {
offset += this.lYearDays(i)
}
var leap = 0; var isAdd = false
for (var i = 1; i < m; i++) {
leap = this.leapMonth(y)
if (!isAdd) { // 处理闰月
if (leap <= i && leap > 0) {
offset += this.leapDays(y); isAdd = true
}
}
offset += this.monthDays(y, i)
}
// 转换闰月农历 需补充该年闰月的前一个月的时差
if (isLeapMonth) { offset += day }
// 1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点)
var stmap = Date.UTC(1900, 1, 30, 0, 0, 0)
var calObj = new Date((offset + d - 31) * 86400000 + stmap)
var cY = calObj.getUTCFullYear()
var cM = calObj.getUTCMonth() + 1
var cD = calObj.getUTCDate()
return this.solar2lunar(cY, cM, cD)
}
}
export default calendar

View File

@@ -0,0 +1,12 @@
{
"uni-calender.ok": "ok",
"uni-calender.cancel": "cancel",
"uni-calender.today": "today",
"uni-calender.MON": "MON",
"uni-calender.TUE": "TUE",
"uni-calender.WED": "WED",
"uni-calender.THU": "THU",
"uni-calender.FRI": "FRI",
"uni-calender.SAT": "SAT",
"uni-calender.SUN": "SUN"
}

View File

@@ -0,0 +1,8 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
import zhHant from './zh-Hant.json'
export default {
en,
'zh-Hans': zhHans,
'zh-Hant': zhHant
}

View File

@@ -0,0 +1,12 @@
{
"uni-calender.ok": "确定",
"uni-calender.cancel": "取消",
"uni-calender.today": "今日",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六"
}

View File

@@ -0,0 +1,12 @@
{
"uni-calender.ok": "確定",
"uni-calender.cancel": "取消",
"uni-calender.today": "今日",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六"
}

View File

@@ -0,0 +1,187 @@
<template>
<view class="uni-calendar-item__weeks-box" :class="{
'uni-calendar-item--disable':weeks.disable,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':(calendar.fullDate === weeks.fullDate && !weeks.isDay) ,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
}"
@click="choiceDate(weeks)">
<view class="uni-calendar-item__weeks-box-item">
<text v-if="selected&&weeks.extraInfo" class="uni-calendar-item__weeks-box-circle"></text>
<text class="uni-calendar-item__weeks-box-text" :class="{
'uni-calendar-item--isDay-text': weeks.isDay,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && !weeks.isDay,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">{{weeks.date}}</text>
<text v-if="!lunar&&!weeks.extraInfo && weeks.isDay" class="uni-calendar-item__weeks-lunar-text" :class="{
'uni-calendar-item--isDay-text':weeks.isDay,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && !weeks.isDay,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
}">{{todayText}}</text>
<text v-if="lunar&&!weeks.extraInfo" class="uni-calendar-item__weeks-lunar-text" :class="{
'uni-calendar-item--isDay-text':weeks.isDay,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && !weeks.isDay,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">{{weeks.isDay ? todayText : (weeks.lunar.IDayCn === '初一'?weeks.lunar.IMonthCn:weeks.lunar.IDayCn)}}</text>
<text v-if="weeks.extraInfo&&weeks.extraInfo.info" class="uni-calendar-item__weeks-lunar-text" :class="{
'uni-calendar-item--extra':weeks.extraInfo.info,
'uni-calendar-item--isDay-text':weeks.isDay,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && !weeks.isDay,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">{{weeks.extraInfo.info}}</text>
</view>
</view>
</template>
<script>
import { initVueI18n } from '@dcloudio/uni-i18n'
import i18nMessages from './i18n/index.js'
const { t } = initVueI18n(i18nMessages)
export default {
emits:['change'],
props: {
weeks: {
type: Object,
default () {
return {}
}
},
calendar: {
type: Object,
default: () => {
return {}
}
},
selected: {
type: Array,
default: () => {
return []
}
},
lunar: {
type: Boolean,
default: false
}
},
computed: {
todayText() {
return t("uni-calender.today")
},
},
methods: {
choiceDate(weeks) {
this.$emit('change', weeks)
}
}
}
</script>
<style lang="scss" scoped>
$uni-font-size-base:14px;
$uni-text-color:#333;
$uni-font-size-sm:12px;
$uni-color-error: #e43d33;
$uni-opacity-disabled: 0.3;
$uni-text-color-disable:#c0c0c0;
$uni-primary: #2979ff !default;
.uni-calendar-item__weeks-box {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
}
.uni-calendar-item__weeks-box-text {
font-size: $uni-font-size-base;
color: $uni-text-color;
}
.uni-calendar-item__weeks-lunar-text {
font-size: $uni-font-size-sm;
color: $uni-text-color;
}
.uni-calendar-item__weeks-box-item {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
width: 100rpx;
height: 100rpx;
}
.uni-calendar-item__weeks-box-circle {
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
border-radius: 8px;
background-color: $uni-color-error;
}
.uni-calendar-item--disable {
background-color: rgba(249, 249, 249, $uni-opacity-disabled);
color: $uni-text-color-disable;
}
.uni-calendar-item--isDay-text {
color: $uni-primary;
}
.uni-calendar-item--isDay {
background-color: $uni-primary;
opacity: 0.8;
color: #fff;
}
.uni-calendar-item--extra {
color: $uni-color-error;
opacity: 0.8;
}
.uni-calendar-item--checked {
background-color: $uni-primary;
color: #fff;
opacity: 0.8;
}
.uni-calendar-item--multiple {
background-color: $uni-primary;
color: #fff;
opacity: 0.8;
}
.uni-calendar-item--before-checked {
background-color: #ff5a5f;
color: #fff;
}
.uni-calendar-item--after-checked {
background-color: #ff5a5f;
color: #fff;
}
</style>

View File

@@ -0,0 +1,567 @@
<template>
<view class="uni-calendar">
<view v-if="!insert&&show" class="uni-calendar__mask" :class="{'uni-calendar--mask-show':aniMaskShow}" @click="clean"></view>
<view v-if="insert || show" class="uni-calendar__content" :class="{'uni-calendar--fixed':!insert,'uni-calendar--ani-show':aniMaskShow}">
<view v-if="!insert" class="uni-calendar__header uni-calendar--fixed-top">
<view class="uni-calendar__header-btn-box" @click="close">
<text class="uni-calendar__header-text uni-calendar--fixed-width">{{cancelText}}</text>
</view>
<view class="uni-calendar__header-btn-box" @click="confirm">
<text class="uni-calendar__header-text uni-calendar--fixed-width">{{okText}}</text>
</view>
</view>
<view class="uni-calendar__header">
<view class="uni-calendar__header-btn-box" @click.stop="pre">
<view class="uni-calendar__header-btn uni-calendar--left"></view>
</view>
<picker mode="date" :value="date" fields="month" @change="bindDateChange">
<text class="uni-calendar__header-text">{{ (nowDate.year||'') +' / '+( nowDate.month||'')}}</text>
</picker>
<view class="uni-calendar__header-btn-box" @click.stop="next">
<view class="uni-calendar__header-btn uni-calendar--right"></view>
</view>
<text class="uni-calendar__backtoday" @click="backToday">{{todayText}}</text>
</view>
<view class="uni-calendar__box">
<view v-if="showMonth" class="uni-calendar__box-bg">
<text class="uni-calendar__box-bg-text">{{nowDate.month}}</text>
</view>
<view class="uni-calendar__weeks">
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SUNText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{monText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{TUEText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{WEDText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{THUText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{FRIText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SATText}}</text>
</view>
</view>
<view class="uni-calendar__weeks" v-for="(item,weekIndex) in weeks" :key="weekIndex">
<view class="uni-calendar__weeks-item" v-for="(weeks,weeksIndex) in item" :key="weeksIndex">
<calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar" :selected="selected" :lunar="lunar" @change="choiceDate"></calendar-item>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import Calendar from './util.js';
import CalendarItem from './uni-calendar-item.vue'
import { initVueI18n } from '@dcloudio/uni-i18n'
import i18nMessages from './i18n/index.js'
const { t } = initVueI18n(i18nMessages)
/**
* Calendar 日历
* @description 日历组件可以查看日期,选择任意范围内的日期,打点操作。常用场景如:酒店日期预订、火车机票选择购买日期、上下班打卡等
* @tutorial https://ext.dcloud.net.cn/plugin?id=56
* @property {String} date 自定义当前时间,默认为今天
* @property {Boolean} lunar 显示农历
* @property {String} startDate 日期选择范围-开始日期
* @property {String} endDate 日期选择范围-结束日期
* @property {Boolean} range 范围选择
* @property {Boolean} insert = [true|false] 插入模式,默认为false
* @value true 弹窗模式
* @value false 插入模式
* @property {Boolean} clearDate = [true|false] 弹窗模式是否清空上次选择内容
* @property {Array} selected 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}]
* @property {Boolean} showMonth 是否选择月份为背景
* @event {Function} change 日期改变,`insert :ture` 时生效
* @event {Function} confirm 确认选择`insert :false` 时生效
* @event {Function} monthSwitch 切换月份时触发
* @example <uni-calendar :insert="true":lunar="true" :start-date="'2019-3-2'":end-date="'2019-5-20'"@change="change" />
*/
export default {
components: {
CalendarItem
},
emits:['close','confirm','change','monthSwitch'],
props: {
date: {
type: String,
default: ''
},
selected: {
type: Array,
default () {
return []
}
},
lunar: {
type: Boolean,
default: false
},
startDate: {
type: String,
default: ''
},
endDate: {
type: String,
default: ''
},
range: {
type: Boolean,
default: false
},
insert: {
type: Boolean,
default: true
},
showMonth: {
type: Boolean,
default: true
},
clearDate: {
type: Boolean,
default: true
}
},
data() {
return {
show: false,
weeks: [],
calendar: {},
nowDate: '',
aniMaskShow: false
}
},
computed:{
/**
* for i18n
*/
okText() {
return t("uni-calender.ok")
},
cancelText() {
return t("uni-calender.cancel")
},
todayText() {
return t("uni-calender.today")
},
monText() {
return t("uni-calender.MON")
},
TUEText() {
return t("uni-calender.TUE")
},
WEDText() {
return t("uni-calender.WED")
},
THUText() {
return t("uni-calender.THU")
},
FRIText() {
return t("uni-calender.FRI")
},
SATText() {
return t("uni-calender.SAT")
},
SUNText() {
return t("uni-calender.SUN")
},
},
watch: {
date(newVal) {
// this.cale.setDate(newVal)
this.init(newVal)
},
startDate(val){
this.cale.resetSatrtDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
endDate(val){
this.cale.resetEndDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
selected(newVal) {
this.cale.setSelectInfo(this.nowDate.fullDate, newVal)
this.weeks = this.cale.weeks
}
},
created() {
this.cale = new Calendar({
selected: this.selected,
startDate: this.startDate,
endDate: this.endDate,
range: this.range,
})
this.init(this.date)
},
methods: {
// 取消穿透
clean() {},
bindDateChange(e) {
const value = e.detail.value + '-1'
this.setDate(value)
const { year,month } = this.cale.getDate(value)
this.$emit('monthSwitch', {
year,
month
})
},
/**
* 初始化日期显示
* @param {Object} date
*/
init(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.calendar = this.cale.getInfo(date)
},
/**
* 打开日历弹窗
*/
open() {
// 弹窗模式并且清理数据
if (this.clearDate && !this.insert) {
this.cale.cleanMultipleStatus()
// this.cale.setDate(this.date)
this.init(this.date)
}
this.show = true
this.$nextTick(() => {
setTimeout(() => {
this.aniMaskShow = true
}, 50)
})
},
/**
* 关闭日历弹窗
*/
close() {
this.aniMaskShow = false
this.$nextTick(() => {
setTimeout(() => {
this.show = false
this.$emit('close')
}, 300)
})
},
/**
* 确认按钮
*/
confirm() {
this.setEmit('confirm')
this.close()
},
/**
* 变化触发
*/
change() {
if (!this.insert) return
this.setEmit('change')
},
/**
* 选择月份触发
*/
monthSwitch() {
let {
year,
month
} = this.nowDate
this.$emit('monthSwitch', {
year,
month: Number(month)
})
},
/**
* 派发事件
* @param {Object} name
*/
setEmit(name) {
let {
year,
month,
date,
fullDate,
lunar,
extraInfo
} = this.calendar
this.$emit(name, {
range: this.cale.multipleStatus,
year,
month,
date,
fulldate: fullDate,
lunar,
extraInfo: extraInfo || {}
})
},
/**
* 选择天触发
* @param {Object} weeks
*/
choiceDate(weeks) {
if (weeks.disable) return
this.calendar = weeks
// 设置多选
this.cale.setMultiple(this.calendar.fullDate)
this.weeks = this.cale.weeks
this.change()
},
/**
* 回到今天
*/
backToday() {
const nowYearMonth = `${this.nowDate.year}-${this.nowDate.month}`
const date = this.cale.getDate(new Date())
const todayYearMonth = `${date.year}-${date.month}`
this.init(date.fullDate)
if(nowYearMonth !== todayYearMonth) {
this.monthSwitch()
}
this.change()
},
/**
* 上个月
*/
pre() {
const preDate = this.cale.getDate(this.nowDate.fullDate, -1, 'month').fullDate
this.setDate(preDate)
this.monthSwitch()
},
/**
* 下个月
*/
next() {
const nextDate = this.cale.getDate(this.nowDate.fullDate, +1, 'month').fullDate
this.setDate(nextDate)
this.monthSwitch()
},
/**
* 设置日期
* @param {Object} date
*/
setDate(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.cale.getInfo(date)
}
}
}
</script>
<style lang="scss" scoped>
$uni-bg-color-mask: rgba($color: #000000, $alpha: 0.4);
$uni-border-color: #EDEDED;
$uni-text-color: #333;
$uni-bg-color-hover:#f1f1f1;
$uni-font-size-base:14px;
$uni-text-color-placeholder: #808080;
$uni-color-subtitle: #555555;
$uni-text-color-grey:#999;
.uni-calendar {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
}
.uni-calendar__mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: $uni-bg-color-mask;
transition-property: opacity;
transition-duration: 0.3s;
opacity: 0;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-calendar--mask-show {
opacity: 1
}
.uni-calendar--fixed {
position: fixed;
/* #ifdef APP-NVUE */
bottom: 0;
/* #endif */
left: 0;
right: 0;
transition-property: transform;
transition-duration: 0.3s;
transform: translateY(460px);
/* #ifndef APP-NVUE */
bottom: calc(var(--window-bottom));
z-index: 99;
/* #endif */
}
.uni-calendar--ani-show {
transform: translateY(0);
}
.uni-calendar__content {
background-color: #fff;
}
.uni-calendar__header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
height: 50px;
border-bottom-color: $uni-border-color;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.uni-calendar--fixed-top {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
border-top-color: $uni-border-color;
border-top-style: solid;
border-top-width: 1px;
}
.uni-calendar--fixed-width {
width: 50px;
}
.uni-calendar__backtoday {
position: absolute;
right: 0;
top: 25rpx;
padding: 0 5px;
padding-left: 10px;
height: 25px;
line-height: 25px;
font-size: 12px;
border-top-left-radius: 25px;
border-bottom-left-radius: 25px;
color: $uni-text-color;
background-color: $uni-bg-color-hover;
}
.uni-calendar__header-text {
text-align: center;
width: 100px;
font-size: $uni-font-size-base;
color: $uni-text-color;
}
.uni-calendar__header-btn-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
}
.uni-calendar__header-btn {
width: 10px;
height: 10px;
border-left-color: $uni-text-color-placeholder;
border-left-style: solid;
border-left-width: 2px;
border-top-color: $uni-color-subtitle;
border-top-style: solid;
border-top-width: 2px;
}
.uni-calendar--left {
transform: rotate(-45deg);
}
.uni-calendar--right {
transform: rotate(135deg);
}
.uni-calendar__weeks {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-calendar__weeks-item {
flex: 1;
}
.uni-calendar__weeks-day {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
height: 45px;
border-bottom-color: #F5F5F5;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.uni-calendar__weeks-day-text {
font-size: 14px;
}
.uni-calendar__box {
position: relative;
}
.uni-calendar__box-bg {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.uni-calendar__box-bg-text {
font-size: 200px;
font-weight: bold;
color: $uni-text-color-grey;
opacity: 0.1;
text-align: center;
/* #ifndef APP-NVUE */
line-height: 1;
/* #endif */
}
</style>

View File

@@ -0,0 +1,360 @@
import CALENDAR from './calendar.js'
class Calendar {
constructor({
date,
selected,
startDate,
endDate,
range
} = {}) {
// 当前日期
this.date = this.getDate(new Date()) // 当前初入日期
// 打点信息
this.selected = selected || [];
// 范围开始
this.startDate = startDate
// 范围结束
this.endDate = endDate
this.range = range
// 多选状态
this.cleanMultipleStatus()
// 每周日期
this.weeks = {}
// this._getWeek(this.date.fullDate)
}
/**
* 设置日期
* @param {Object} date
*/
setDate(date) {
this.selectDate = this.getDate(date)
this._getWeek(this.selectDate.fullDate)
}
/**
* 清理多选状态
*/
cleanMultipleStatus() {
this.multipleStatus = {
before: '',
after: '',
data: []
}
}
/**
* 重置开始日期
*/
resetSatrtDate(startDate) {
// 范围开始
this.startDate = startDate
}
/**
* 重置结束日期
*/
resetEndDate(endDate) {
// 范围结束
this.endDate = endDate
}
/**
* 获取任意时间
*/
getDate(date, AddDayCount = 0, str = 'day') {
if (!date) {
date = new Date()
}
if (typeof date !== 'object') {
date = date.replace(/-/g, '/')
}
const dd = new Date(date)
switch (str) {
case 'day':
dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期
break
case 'month':
if (dd.getDate() === 31 && AddDayCount>0) {
dd.setDate(dd.getDate() + AddDayCount)
} else {
const preMonth = dd.getMonth()
dd.setMonth(preMonth + AddDayCount) // 获取AddDayCount天后的日期
const nextMonth = dd.getMonth()
// 处理 pre 切换月份目标月份为2月没有当前日(30 31) 切换错误问题
if(AddDayCount<0 && preMonth!==0 && nextMonth-preMonth>AddDayCount){
dd.setMonth(nextMonth+(nextMonth-preMonth+AddDayCount))
}
// 处理 next 切换月份目标月份为2月没有当前日(30 31) 切换错误问题
if(AddDayCount>0 && nextMonth-preMonth>AddDayCount){
dd.setMonth(nextMonth-(nextMonth-preMonth-AddDayCount))
}
}
break
case 'year':
dd.setFullYear(dd.getFullYear() + AddDayCount) // 获取AddDayCount天后的日期
break
}
const y = dd.getFullYear()
const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期,不足10补0
const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号,不足10补0
return {
fullDate: y + '-' + m + '-' + d,
year: y,
month: m,
date: d,
day: dd.getDay()
}
}
/**
* 获取上月剩余天数
*/
_getLastMonthDays(firstDay, full) {
let dateArr = []
for (let i = firstDay; i > 0; i--) {
const beforeDate = new Date(full.year, full.month - 1, -i + 1).getDate()
dateArr.push({
date: beforeDate,
month: full.month - 1,
lunar: this.getlunar(full.year, full.month - 1, beforeDate),
disable: true
})
}
return dateArr
}
/**
* 获取本月天数
*/
_currentMonthDys(dateData, full) {
let dateArr = []
let fullDate = this.date.fullDate
for (let i = 1; i <= dateData; i++) {
let nowDate = full.year + '-' + (full.month < 10 ?
full.month : full.month) + '-' + (i < 10 ?
'0' + i : i)
// 是否今天
let isDay = fullDate === nowDate
// 获取打点信息
let info = this.selected && this.selected.find((item) => {
if (this.dateEqual(nowDate, item.date)) {
return item
}
})
// 日期禁用
let disableBefore = true
let disableAfter = true
if (this.startDate) {
// let dateCompBefore = this.dateCompare(this.startDate, fullDate)
// disableBefore = this.dateCompare(dateCompBefore ? this.startDate : fullDate, nowDate)
disableBefore = this.dateCompare(this.startDate, nowDate)
}
if (this.endDate) {
// let dateCompAfter = this.dateCompare(fullDate, this.endDate)
// disableAfter = this.dateCompare(nowDate, dateCompAfter ? this.endDate : fullDate)
disableAfter = this.dateCompare(nowDate, this.endDate)
}
let multiples = this.multipleStatus.data
let checked = false
let multiplesStatus = -1
if (this.range) {
if (multiples) {
multiplesStatus = multiples.findIndex((item) => {
return this.dateEqual(item, nowDate)
})
}
if (multiplesStatus !== -1) {
checked = true
}
}
let data = {
fullDate: nowDate,
year: full.year,
date: i,
multiple: this.range ? checked : false,
beforeMultiple: this.dateEqual(this.multipleStatus.before, nowDate),
afterMultiple: this.dateEqual(this.multipleStatus.after, nowDate),
month: full.month,
lunar: this.getlunar(full.year, full.month, i),
disable: !(disableBefore && disableAfter),
isDay
}
if (info) {
data.extraInfo = info
}
dateArr.push(data)
}
return dateArr
}
/**
* 获取下月天数
*/
_getNextMonthDays(surplus, full) {
let dateArr = []
for (let i = 1; i < surplus + 1; i++) {
dateArr.push({
date: i,
month: Number(full.month) + 1,
lunar: this.getlunar(full.year, Number(full.month) + 1, i),
disable: true
})
}
return dateArr
}
/**
* 获取当前日期详情
* @param {Object} date
*/
getInfo(date) {
if (!date) {
date = new Date()
}
const dateInfo = this.canlender.find(item => item.fullDate === this.getDate(date).fullDate)
return dateInfo
}
/**
* 比较时间大小
*/
dateCompare(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
if (startDate <= endDate) {
return true
} else {
return false
}
}
/**
* 比较时间是否相等
*/
dateEqual(before, after) {
// 计算截止时间
before = new Date(before.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
after = new Date(after.replace('-', '/').replace('-', '/'))
if (before.getTime() - after.getTime() === 0) {
return true
} else {
return false
}
}
/**
* 获取日期范围内所有日期
* @param {Object} begin
* @param {Object} end
*/
geDateAll(begin, end) {
var arr = []
var ab = begin.split('-')
var ae = end.split('-')
var db = new Date()
db.setFullYear(ab[0], ab[1] - 1, ab[2])
var de = new Date()
de.setFullYear(ae[0], ae[1] - 1, ae[2])
var unixDb = db.getTime() - 24 * 60 * 60 * 1000
var unixDe = de.getTime() - 24 * 60 * 60 * 1000
for (var k = unixDb; k <= unixDe;) {
k = k + 24 * 60 * 60 * 1000
arr.push(this.getDate(new Date(parseInt(k))).fullDate)
}
return arr
}
/**
* 计算阴历日期显示
*/
getlunar(year, month, date) {
return CALENDAR.solar2lunar(year, month, date)
}
/**
* 设置打点
*/
setSelectInfo(data, value) {
this.selected = value
this._getWeek(data)
}
/**
* 获取多选状态
*/
setMultiple(fullDate) {
let {
before,
after
} = this.multipleStatus
if (!this.range) return
if (before && after) {
this.multipleStatus.before = fullDate
this.multipleStatus.after = ''
this.multipleStatus.data = []
} else {
if (!before) {
this.multipleStatus.before = fullDate
} else {
this.multipleStatus.after = fullDate
if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) {
this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after);
} else {
this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before);
}
}
}
this._getWeek(fullDate)
}
/**
* 获取每周数据
* @param {Object} dateData
*/
_getWeek(dateData) {
const {
year,
month
} = this.getDate(dateData)
let firstDay = new Date(year, month - 1, 1).getDay()
let currentDay = new Date(year, month, 0).getDate()
let dates = {
lastMonthDays: this._getLastMonthDays(firstDay, this.getDate(dateData)), // 上个月末尾几天
currentMonthDys: this._currentMonthDys(currentDay, this.getDate(dateData)), // 本月天数
nextMonthDays: [], // 下个月开始几天
weeks: []
}
let canlender = []
const surplus = 42 - (dates.lastMonthDays.length + dates.currentMonthDys.length)
dates.nextMonthDays = this._getNextMonthDays(surplus, this.getDate(dateData))
canlender = canlender.concat(dates.lastMonthDays, dates.currentMonthDys, dates.nextMonthDays)
let weeks = {}
// 拼接数组 上个月开始几天 + 本月天数+ 下个月开始几天
for (let i = 0; i < canlender.length; i++) {
if (i % 7 === 0) {
weeks[parseInt(i / 7)] = new Array(7)
}
weeks[parseInt(i / 7)][i % 7] = canlender[i]
}
this.canlender = canlender
this.weeks = weeks
}
//静态方法
// static init(date) {
// if (!this.instance) {
// this.instance = new Calendar(date);
// }
// return this.instance;
// }
}
export default Calendar

View File

@@ -0,0 +1,86 @@
{
"id": "uni-calendar",
"displayName": "uni-calendar 日历",
"version": "1.4.12",
"description": "日历组件",
"keywords": [
"uni-ui",
"uniui",
"日历",
"",
"打卡",
"日历选择"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,103 @@
## Calendar 日历
> **组件名:uni-calendar**
> 代码块: `uCalendar`
日历组件
> **注意事项**
> 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。
> - 本组件农历转换使用的js是 [@1900-2100区间内的公历、农历互转](https://github.com/jjonline/calendar.js)
> - 仅支持自定义组件模式
> - `date`属性传入的应该是一个 String ,如: 2019-06-27 ,而不是 new Date()
> - 通过 `insert` 属性来确定当前的事件是 @change 还是 @confirm 。理应合并为一个事件,但是为了区分模式,现使用两个事件,这里需要注意
> - 弹窗模式下无法阻止后面的元素滚动,如有需要阻止,请在弹窗弹出后,手动设置滚动元素为不可滚动
### 安装方式
本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。
如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55)
### 基本用法
在 ``template`` 中使用组件
```html
<view>
<uni-calendar
:insert="true"
:lunar="true"
:start-date="'2019-3-2'"
:end-date="'2019-5-20'"
@change="change"
/>
</view>
```
### 通过方法打开日历
需要设置 `insert` 为 `false`
```html
<view>
<uni-calendar
ref="calendar"
:insert="false"
@confirm="confirm"
/>
<button @click="open">打开日历</button>
</view>
```
```javascript
export default {
data() {
return {};
},
methods: {
open(){
this.$refs.calendar.open();
},
confirm(e) {
console.log(e);
}
}
};
```
## API
### Calendar Props
| 属性名 | 类型 | 默认值| 说明 |
| - | - | - | - |
| date | String |- | 自定义当前时间,默认为今天 |
| lunar | Boolean | false | 显示农历 |
| startDate | String |- | 日期选择范围-开始日期 |
| endDate | String |- | 日期选择范围-结束日期 |
| range | Boolean | false | 范围选择 |
| insert | Boolean | false | 插入模式,可选值,ture:插入模式;false:弹窗模式;默认为插入模式 |
|clearDate |Boolean |true |弹窗模式是否清空上次选择内容 |
| selected | Array |- | 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}] |
|showMonth | Boolean | true | 是否显示月份为背景 |
### Calendar Events
| 事件名 | 说明 |返回值|
| - | - | - |
| open | 弹出日历组件,`insert :false` 时生效|- |
## 组件示例
点击查看:[https://hellouniapp.dcloud.net.cn/pages/extUI/calendar/calendar](https://hellouniapp.dcloud.net.cn/pages/extUI/calendar/calendar)

View File

@@ -0,0 +1,26 @@
## 1.3.1(2021-12-20)
- 修复 在vue页面下略缩图显示不正常的bug
## 1.3.0(2021-11-19)
- 重构插槽的用法 ,header 替换为 title
- 新增 actions 插槽
- 新增 cover 封面图属性和插槽
- 新增 padding 内容默认内边距离
- 新增 margin 卡片默认外边距离
- 新增 spacing 卡片默认内边距
- 新增 shadow 卡片阴影属性
- 取消 mode 属性,可使用组合插槽代替
- 取消 note 属性 ,使用actions插槽代替
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-card](https://uniapp.dcloud.io/component/uniui/uni-card)
## 1.2.1(2021-07-30)
- 优化 vue3下事件警告的问题
## 1.2.0(2021-07-13)
- 组件兼容 vue3,如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.8(2021-07-01)
- 优化 图文卡片无图片加载时,提供占位图标
- 新增 header 插槽,自定义卡片头部( 图文卡片 mode="style" 时,不支持)
- 修复 thumbnail 不存在仍然占位的 bug
## 1.1.7(2021-05-12)
- 新增 组件示例地址
## 1.1.6(2021-02-04)
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,270 @@
<template>
<view class="uni-card" :class="{ 'uni-card--full': isFull, 'uni-card--shadow': isShadow,'uni-card--border':border}"
:style="{'margin':isFull?0:margin,'padding':spacing,'box-shadow':isShadow?shadow:''}">
<!-- 封面 -->
<slot name="cover">
<view v-if="cover" class="uni-card__cover">
<image class="uni-card__cover-image" mode="widthFix" @click="onClick('cover')" :src="cover"></image>
</view>
</slot>
<slot name="title">
<view v-if="title || extra" class="uni-card__header">
<!-- 卡片标题 -->
<view class="uni-card__header-box" @click="onClick('title')">
<view v-if="thumbnail" class="uni-card__header-avatar">
<image class="uni-card__header-avatar-image" :src="thumbnail" mode="aspectFit" />
</view>
<view class="uni-card__header-content">
<text class="uni-card__header-content-title uni-ellipsis">{{ title }}</text>
<text v-if="title&&subTitle"
class="uni-card__header-content-subtitle uni-ellipsis">{{ subTitle }}</text>
</view>
</view>
<view class="uni-card__header-extra" @click="onClick('extra')">
<text class="uni-card__header-extra-text">{{ extra }}</text>
</view>
</view>
</slot>
<!-- 卡片内容 -->
<view class="uni-card__content" :style="{padding:padding}" @click="onClick('content')">
<slot></slot>
</view>
<view class="uni-card__actions" @click="onClick('actions')">
<slot name="actions"></slot>
</view>
</view>
</template>
<script>
/**
* Card 卡片
* @description 卡片视图组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=22
* @property {String} title 标题文字
* @property {String} subTitle 副标题
* @property {Number} padding 内容内边距
* @property {Number} margin 卡片外边距
* @property {Number} spacing 卡片内边距
* @property {String} extra 标题额外信息
* @property {String} cover 封面图(本地路径需要引入)
* @property {String} thumbnail 标题左侧缩略图
* @property {Boolean} is-full = [true | false] 卡片内容是否通栏,为 true 时将去除padding值
* @property {Boolean} is-shadow = [true | false] 卡片内容是否开启阴影
* @property {String} shadow 卡片阴影
* @property {Boolean} border 卡片边框
* @event {Function} click 点击 Card 触发事件
*/
export default {
name: 'UniCard',
emits: ['click'],
props: {
title: {
type: String,
default: ''
},
subTitle: {
type: String,
default: ''
},
padding: {
type: String,
default: '10px'
},
margin: {
type: String,
default: '15px'
},
spacing: {
type: String,
default: '0 10px'
},
extra: {
type: String,
default: ''
},
cover: {
type: String,
default: ''
},
thumbnail: {
type: String,
default: ''
},
isFull: {
// 内容区域是否通栏
type: Boolean,
default: false
},
isShadow: {
// 是否开启阴影
type: Boolean,
default: true
},
shadow: {
type: String,
default: '0px 0px 3px 1px rgba(0, 0, 0, 0.08)'
},
border: {
type: Boolean,
default: true
}
},
methods: {
onClick(type) {
this.$emit('click', type)
}
}
}
</script>
<style lang="scss">
$uni-border-3: #EBEEF5 !default;
$uni-shadow-base:0 0px 6px 1px rgba($color: #a5a5a5, $alpha: 0.2) !default;
$uni-main-color: #3a3a3a !default;
$uni-base-color: #6a6a6a !default;
$uni-secondary-color: #909399 !default;
$uni-spacing-sm: 8px !default;
$uni-border-color:$uni-border-3;
$uni-shadow: $uni-shadow-base;
$uni-card-title: 15px;
$uni-cart-title-color:$uni-main-color;
$uni-card-subtitle: 12px;
$uni-cart-subtitle-color:$uni-secondary-color;
$uni-card-spacing: 10px;
$uni-card-content-color: $uni-base-color;
.uni-card {
margin: $uni-card-spacing;
padding: 0 $uni-spacing-sm;
border-radius: 4px;
overflow: hidden;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
background-color: #fff;
flex: 1;
.uni-card__cover {
position: relative;
margin-top: $uni-card-spacing;
flex-direction: row;
overflow: hidden;
border-radius: 4px;
.uni-card__cover-image {
flex: 1;
// width: 100%;
/* #ifndef APP-PLUS */
vertical-align: middle;
/* #endif */
}
}
.uni-card__header {
display: flex;
border-bottom: 1px $uni-border-color solid;
flex-direction: row;
align-items: center;
padding: $uni-card-spacing;
overflow: hidden;
.uni-card__header-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
overflow: hidden;
}
.uni-card__header-avatar {
width: 40px;
height: 40px;
overflow: hidden;
border-radius: 5px;
margin-right: $uni-card-spacing;
.uni-card__header-avatar-image {
flex: 1;
width: 40px;
height: 40px;
}
}
.uni-card__header-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
flex: 1;
// height: 40px;
overflow: hidden;
.uni-card__header-content-title {
font-size: $uni-card-title;
color: $uni-cart-title-color;
// line-height: 22px;
}
.uni-card__header-content-subtitle {
font-size: $uni-card-subtitle;
margin-top: 5px;
color: $uni-cart-subtitle-color;
}
}
.uni-card__header-extra {
line-height: 12px;
.uni-card__header-extra-text {
font-size: 12px;
color: $uni-cart-subtitle-color;
}
}
}
.uni-card__content {
padding: $uni-card-spacing;
font-size: 14px;
color: $uni-card-content-color;
line-height: 22px;
}
.uni-card__actions {
font-size: 12px;
}
}
.uni-card--border {
border: 1px solid $uni-border-color;
}
.uni-card--shadow {
position: relative;
/* #ifndef APP-NVUE */
box-shadow: $uni-shadow;
/* #endif */
}
.uni-card--full {
margin: 0;
border-left-width: 0;
border-left-width: 0;
border-radius: 0;
}
/* #ifndef APP-NVUE */
.uni-card--full:after {
border-radius: 0;
}
/* #endif */
.uni-ellipsis {
/* #ifndef APP-NVUE */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* #endif */
/* #ifdef APP-NVUE */
lines: 1;
/* #endif */
}
</style>

View File

@@ -0,0 +1,90 @@
{
"id": "uni-card",
"displayName": "uni-card 卡片",
"version": "1.3.1",
"description": "Card 组件,提供常见的卡片样式。",
"keywords": [
"uni-ui",
"uniui",
"card",
"",
"卡片"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": [
"uni-icons",
"uni-scss"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
## Card 卡片
> **组件名:uni-card**
> 代码块: `uCard`
卡片视图组件。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-card)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

View File

@@ -0,0 +1,48 @@
## 1.4.8(2025-09-16)
- 修复 modelValue 修改会两次触发 change 事件的 Bug
## 1.4.7(2025-09-11)
- 修复 modelValue 修改不会触发更新的 Bug
## 1.4.6(2025-09-02)
- 修复 modelValue 修改不会触发 change 事件的 Bug
## 1.4.5(2025-09-02)
- 修复 非手风琴模式 不能设置 modeValue 为 [] 的 Bug (question/205130)
## 1.4.4(2024-03-20)
- 修复 titleBorder类型修正
## 1.4.3(2022-01-25)
- 修复 初始化的时候 ,open 属性失效的bug
## 1.4.2(2022-01-21)
- 修复 微信小程序resize后组件收起的bug
## 1.4.1(2021-11-22)
- 修复 vue3中个别scss变量无法找到的问题
## 1.4.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-collapse](https://uniapp.dcloud.io/component/uniui/uni-collapse)
## 1.3.3(2021-08-17)
- 优化 show-arrow 属性默认为true
## 1.3.2(2021-08-17)
- 新增 show-arrow 属性,控制是否显示右侧箭头
## 1.3.1(2021-07-30)
- 优化 vue3下小程序事件警告的问题
## 1.3.0(2021-07-30)
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.2.2(2021-07-21)
- 修复 由1.2.0版本引起的 change 事件返回 undefined 的Bug
## 1.2.1(2021-07-21)
- 优化 组件示例
## 1.2.0(2021-07-21)
- 新增 组件折叠动画
- 新增 value\v-model 属性 ,动态修改面板折叠状态
- 新增 title 插槽 ,可定义面板标题
- 新增 border 属性 ,显示隐藏面板内容分隔线
- 新增 title-border 属性 ,显示隐藏面板标题分隔线
- 修复 resize 方法失效的Bug
- 修复 change 事件返回参数不正确的Bug
- 优化 H5、App 平台自动更具内容更新高度,无需调用 reszie() 方法
## 1.1.7(2021-05-12)
- 新增 组件示例地址
## 1.1.6(2021-02-05)
- 优化 组件引用关系,通过uni_modules引用组件
## 1.1.5(2021-02-05)
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,402 @@
<template>
<view class="uni-collapse-item">
<!-- onClick(!isOpen) -->
<view @click="onClick(!isOpen)" class="uni-collapse-item__title"
:class="{'is-open':isOpen &&titleBorder === 'auto' ,'uni-collapse-item-border':titleBorder !== 'none'}">
<view class="uni-collapse-item__title-wrap">
<slot name="title">
<view class="uni-collapse-item__title-box" :class="{'is-disabled':disabled}">
<image v-if="thumb" :src="thumb" class="uni-collapse-item__title-img" />
<text class="uni-collapse-item__title-text">{{ title }}</text>
</view>
</slot>
</view>
<view v-if="showArrow"
:class="{ 'uni-collapse-item__title-arrow-active': isOpen, 'uni-collapse-item--animation': showAnimation === true }"
class="uni-collapse-item__title-arrow">
<uni-icons :color="disabled?'#ddd':'#bbb'" size="14" type="bottom" />
</view>
</view>
<view class="uni-collapse-item__wrap" :class="{'is--transition':showAnimation}"
:style="{height: (isOpen?height:0) +'px'}">
<view :id="elId" ref="collapse--hook" class="uni-collapse-item__wrap-content"
:class="{open:isheight,'uni-collapse-item--border':border&&isOpen}">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
// #ifdef APP-NVUE
const dom = weex.requireModule('dom')
// #endif
/**
* CollapseItem 折叠面板子组件
* @description 折叠面板子组件
* @property {String} title 标题文字
* @property {String} thumb 标题左侧缩略图
* @property {String} name 唯一标志符
* @property {Boolean} open = [true|false] 是否展开组件
* @property {Boolean} titleBorder = [true|false] 是否显示标题分隔线
* @property {String} border = ['auto'|'show'|'none'] 是否显示分隔线
* @property {Boolean} disabled = [true|false] 是否展开面板
* @property {Boolean} showAnimation = [true|false] 开启动画
* @property {Boolean} showArrow = [true|false] 是否显示右侧箭头
*/
export default {
name: 'uniCollapseItem',
props: {
// 列表标题
title: {
type: String,
default: ''
},
name: {
type: [Number, String],
default: ''
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// #ifdef APP-PLUS
// 是否显示动画,app 端默认不开启动画,卡顿严重
showAnimation: {
type: Boolean,
default: false
},
// #endif
// #ifndef APP-PLUS
// 是否显示动画
showAnimation: {
type: Boolean,
default: true
},
// #endif
// 是否展开
open: {
type: Boolean,
default: false
},
// 缩略图
thumb: {
type: String,
default: ''
},
// 标题分隔线显示类型
titleBorder: {
type: String,
default: 'auto'
},
border: {
type: Boolean,
default: true
},
showArrow: {
type: Boolean,
default: true
}
},
data() {
// TODO 随机生生元素ID,解决百度小程序获取同一个元素位置信息的bug
const elId = `Uni_${Math.ceil(Math.random() * 10e5).toString(36)}`
return {
isOpen: false,
isheight: null,
height: 0,
elId,
nameSync: 0
}
},
watch: {
open(val) {
this.isOpen = val
this.onClick(val, 'init')
}
},
updated(e) {
this.$nextTick(() => {
this.init(true)
})
},
created() {
this.collapse = this.getCollapse()
this.oldHeight = 0
this.onClick(this.open, 'init')
},
// #ifndef VUE3
// TODO vue2
destroyed() {
if (this.__isUnmounted) return
this.uninstall()
},
// #endif
// #ifdef VUE3
// TODO vue3
unmounted() {
this.__isUnmounted = true
this.uninstall()
},
// #endif
mounted() {
if (!this.collapse) return
if (this.name !== '') {
this.nameSync = this.name
} else {
this.nameSync = this.collapse.childrens.length + ''
}
if (this.collapse.names.indexOf(this.nameSync) === -1) {
this.collapse.names.push(this.nameSync)
} else {
console.warn(`name 值 ${this.nameSync} 重复`);
}
if (this.collapse.childrens.indexOf(this) === -1) {
this.collapse.childrens.push(this)
}
this.init()
},
methods: {
init(type) {
// #ifndef APP-NVUE
this.getCollapseHeight(type)
// #endif
// #ifdef APP-NVUE
this.getNvueHwight(type)
// #endif
},
uninstall() {
if (this.collapse) {
this.collapse.childrens.forEach((item, index) => {
if (item === this) {
this.collapse.childrens.splice(index, 1)
}
})
this.collapse.names.forEach((item, index) => {
if (item === this.nameSync) {
this.collapse.names.splice(index, 1)
}
})
}
},
onClick(isOpen, type) {
if (this.disabled) return
this.isOpen = isOpen
if (this.isOpen && this.collapse) {
this.collapse.setAccordion(this)
}
if (type !== 'init') {
this.collapse.onChange(isOpen, this)
}
},
getCollapseHeight(type, index = 0) {
const views = uni.createSelectorQuery().in(this)
views
.select(`#${this.elId}`)
.fields({
size: true
}, data => {
// TODO 百度中可能获取不到节点信息 ,需要循环获取
if (index >= 10) return
if (!data) {
index++
this.getCollapseHeight(false, index)
return
}
// #ifdef APP-NVUE
this.height = data.height + 1
// #endif
// #ifndef APP-NVUE
this.height = data.height
// #endif
this.isheight = true
if (type) return
this.onClick(this.isOpen, 'init')
})
.exec()
},
getNvueHwight(type) {
const result = dom.getComponentRect(this.$refs['collapse--hook'], option => {
if (option && option.result && option.size) {
// #ifdef APP-NVUE
this.height = option.size.height + 1
// #endif
// #ifndef APP-NVUE
this.height = option.size.height
// #endif
this.isheight = true
if (type) return
this.onClick(this.open, 'init')
}
})
},
/**
* 获取父元素实例
*/
getCollapse(name = 'uniCollapse') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false;
parentName = parent.$options.name;
}
return parent;
}
}
}
</script>
<style lang="scss">
.uni-collapse-item {
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
&__title {
/* #ifndef APP-NVUE */
display: flex;
width: 100%;
box-sizing: border-box;
/* #endif */
flex-direction: row;
align-items: center;
transition: border-bottom-color .3s;
// transition-property: border-bottom-color;
// transition-duration: 5s;
&-wrap {
width: 100%;
flex: 1;
}
&-box {
padding: 0 15px;
/* #ifndef APP-NVUE */
display: flex;
width: 100%;
box-sizing: border-box;
/* #endif */
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 48px;
line-height: 48px;
background-color: #fff;
color: #303133;
font-size: 13px;
font-weight: 500;
/* #ifdef H5 */
cursor: pointer;
outline: none;
/* #endif */
&.is-disabled {
.uni-collapse-item__title-text {
color: #999;
}
}
}
&.uni-collapse-item-border {
border-bottom: 1px solid #ebeef5;
}
&.is-open {
border-bottom-color: transparent;
}
&-img {
height: 22px;
width: 22px;
margin-right: 10px;
}
&-text {
flex: 1;
font-size: 14px;
/* #ifndef APP-NVUE */
white-space: nowrap;
color: inherit;
/* #endif */
/* #ifdef APP-NVUE */
lines: 1;
/* #endif */
overflow: hidden;
text-overflow: ellipsis;
}
&-arrow {
/* #ifndef APP-NVUE */
display: flex;
box-sizing: border-box;
/* #endif */
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 10px;
transform: rotate(0deg);
&-active {
transform: rotate(-180deg);
}
}
}
&__wrap {
/* #ifndef APP-NVUE */
will-change: height;
box-sizing: border-box;
/* #endif */
background-color: #fff;
overflow: hidden;
position: relative;
height: 0;
&.is--transition {
// transition: all 0.3s;
transition-property: height, border-bottom-width;
transition-duration: 0.3s;
/* #ifndef APP-NVUE */
will-change: height;
/* #endif */
}
&-content {
position: absolute;
font-size: 13px;
color: #303133;
// transition: height 0.3s;
border-bottom-color: transparent;
border-bottom-style: solid;
border-bottom-width: 0;
&.uni-collapse-item--border {
border-bottom-width: 1px;
border-bottom-color: red;
border-bottom-color: #ebeef5;
}
&.open {
position: relative;
}
}
}
&--animation {
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
}
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<view class="uni-collapse">
<slot />
</view>
</template>
<script>
/**
* Collapse 折叠面板
* @description 展示可以折叠 / 展开的内容区域
* @tutorial https://ext.dcloud.net.cn/plugin?id=23
* @property {String|Array} value 当前激活面板改变时触发(如果是手风琴模式,参数类型为string,否则为array)
* @property {Boolean} accordion = [true|false] 是否开启手风琴效果是否开启手风琴效果
* @event {Function} change 切换面板时触发,如果是手风琴模式,返回类型为string,否则为array
*/
export default {
name: 'uniCollapse',
emits:['change','activeItem','input','update:modelValue'],
props: {
value: {
type: [String, Array],
default: ''
},
modelValue: {
type: [String, Array],
default: ''
},
accordion: {
// 是否开启手风琴效果
type: [Boolean, String],
default: false
},
},
data() {
return {}
},
computed: {
// TODO 兼容 vue2 和 vue3
dataValue() {
let value = (typeof this.value === 'string' && this.value === '') ||
(Array.isArray(this.value) && this.value.length === 0)
let modelValue = (typeof this.modelValue === 'string' && this.modelValue === '') ||
(Array.isArray(this.modelValue) && this.modelValue.length === 0)
if (value) {
return this.modelValue
}
if (modelValue) {
return this.value
}
return this.value
}
},
watch: {
dataValue: {
handler(newVal) {
this.setOpen(newVal)
},
deep: true
}
},
created() {
this.childrens = []
this.names = []
},
mounted() {
this.$nextTick(()=>{
this.setOpen(this.dataValue)
})
},
methods: {
setOpen(val) {
const str = typeof val === 'string'
const arr = Array.isArray(val)
this.childrens.forEach((vm) => {
if (str) {
if (val === vm.nameSync) {
if (!this.accordion) {
console.warn('accordion 属性为 false ,v-model 类型应该为 array')
return
}
vm.isOpen = true
}
}
if (arr) {
const isOpen = val.findIndex(v => v === vm.nameSync) !== -1
if (this.accordion && isOpen) {
console.warn('accordion 属性为 true ,v-model 类型应该为 string')
return
}
vm.isOpen = isOpen
}
})
this.emit(val)
},
setAccordion(self) {
if (!this.accordion) return
this.childrens.forEach((vm, index) => {
if (self !== vm) {
vm.isOpen = false
}
})
},
resize() {
this.childrens.forEach((vm, index) => {
// #ifndef APP-NVUE
vm.getCollapseHeight()
// #endif
// #ifdef APP-NVUE
vm.getNvueHwight()
// #endif
})
},
onChange(isOpen, self) {
let activeItem = []
if (this.accordion) {
activeItem = isOpen ? self.nameSync : ''
} else {
this.childrens.forEach((vm, index) => {
if (vm.isOpen) {
activeItem.push(vm.nameSync)
}
})
}
this.$emit('change', activeItem)
this.emit(activeItem)
},
emit(val){
this.$emit('input', val)
this.$emit('update:modelValue', val)
}
}
}
</script>
<style lang="scss" >
.uni-collapse {
/* #ifndef APP-NVUE */
width: 100%;
display: flex;
/* #endif */
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
flex-direction: column;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,106 @@
{
"id": "uni-collapse",
"displayName": "uni-collapse 折叠面板",
"version": "1.4.8",
"description": "Collapse 组件,可以折叠 / 展开的内容区域。",
"keywords": [
"uni-ui",
"折叠",
"折叠面板",
"手风琴"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "",
"uni-app": "^4.07",
"uni-app-x": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [
"uni-scss",
"uni-icons"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "√",
"android": "√",
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√",
"alipay": "√",
"toutiao": "√",
"baidu": "√",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "√",
"lark": "-"
},
"quickapp": {
"huawei": "√",
"union": "√"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
## Collapse 折叠面板
> **组件名:uni-collapse**
> 代码块: `uCollapse`
> 关联组件:`uni-collapse-item`、`uni-icons`。
折叠面板用来折叠/显示过长的内容或者是列表。通常是在多内容分类项使用,折叠不重要的内容,显示重要内容。点击可以展开折叠部分。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-collapse)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

View File

@@ -0,0 +1,17 @@
## 1.0.2(2024-09-21)
- 新增 clearAble属性
## 1.0.1(2021-11-23)
- 优化 label、label-width 属性
## 1.0.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-combox](https://uniapp.dcloud.io/component/uniui/uni-combox)
## 0.1.0(2021-07-30)
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 0.0.6(2021-05-12)
- 新增 组件示例地址
## 0.0.5(2021-04-21)
- 优化 添加依赖 uni-icons, 导入后自动下载依赖
## 0.0.4(2021-02-05)
- 优化 组件引用关系,通过uni_modules引用组件
## 0.0.3(2021-02-04)
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,284 @@
<template>
<view class="uni-combox" :class="border ? '' : 'uni-combox__no-border'">
<view v-if="label" class="uni-combox__label" :style="labelStyle">
<text>{{label}}</text>
</view>
<view class="uni-combox__input-box">
<input class="uni-combox__input" type="text" :placeholder="placeholder" placeholder-class="uni-combox__input-plac"
v-model="inputVal" @input="onInput" @focus="onFocus" @blur="onBlur" />
<uni-icons v-if="!inputVal || !clearAble" :type="showSelector? 'top' : 'bottom'" size="14" color="#999" @click="toggleSelector">
</uni-icons>
<uni-icons v-if="inputVal && clearAble" type="clear" size="24" color="#999" @click="clean">
</uni-icons>
</view>
<view class="uni-combox__selector" v-if="showSelector">
<view class="uni-popper__arrow"></view>
<scroll-view scroll-y="true" class="uni-combox__selector-scroll">
<view class="uni-combox__selector-empty" v-if="filterCandidatesLength === 0">
<text>{{emptyTips}}</text>
</view>
<view class="uni-combox__selector-item" v-for="(item,index) in filterCandidates" :key="index"
@click="onSelectorClick(index)">
<text>{{item}}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
/**
* Combox 组合输入框
* @description 组合输入框一般用于既可以输入也可以选择的场景
* @tutorial https://ext.dcloud.net.cn/plugin?id=1261
* @property {String} label 左侧文字
* @property {String} labelWidth 左侧内容宽度
* @property {String} placeholder 输入框占位符
* @property {Array} candidates 候选项列表
* @property {String} emptyTips 筛选结果为空时显示的文字
* @property {String} value 组合框的值
*/
export default {
name: 'uniCombox',
emits: ['input', 'update:modelValue'],
props: {
clearAble: {
type: Boolean,
default: false
},
border: {
type: Boolean,
default: true
},
label: {
type: String,
default: ''
},
labelWidth: {
type: String,
default: 'auto'
},
placeholder: {
type: String,
default: ''
},
candidates: {
type: Array,
default () {
return []
}
},
emptyTips: {
type: String,
default: '无匹配项'
},
// #ifndef VUE3
value: {
type: [String, Number],
default: ''
},
// #endif
// #ifdef VUE3
modelValue: {
type: [String, Number],
default: ''
},
// #endif
},
data() {
return {
showSelector: false,
inputVal: ''
}
},
computed: {
labelStyle() {
if (this.labelWidth === 'auto') {
return ""
}
return `width: ${this.labelWidth}`
},
filterCandidates() {
return this.candidates.filter((item) => {
return item.toString().indexOf(this.inputVal) > -1
})
},
filterCandidatesLength() {
return this.filterCandidates.length
}
},
watch: {
// #ifndef VUE3
value: {
handler(newVal) {
this.inputVal = newVal
},
immediate: true
},
// #endif
// #ifdef VUE3
modelValue: {
handler(newVal) {
this.inputVal = newVal
},
immediate: true
},
// #endif
},
methods: {
toggleSelector() {
this.showSelector = !this.showSelector
},
onFocus() {
this.showSelector = true
},
onBlur() {
setTimeout(() => {
this.showSelector = false
}, 153)
},
onSelectorClick(index) {
this.inputVal = this.filterCandidates[index]
this.showSelector = false
this.$emit('input', this.inputVal)
this.$emit('update:modelValue', this.inputVal)
},
onInput() {
setTimeout(() => {
this.$emit('input', this.inputVal)
this.$emit('update:modelValue', this.inputVal)
})
},
clean() {
this.inputVal = ''
this.onInput()
}
}
}
</script>
<style lang="scss" scoped>
.uni-combox {
font-size: 14px;
border: 1px solid #DCDFE6;
border-radius: 4px;
padding: 6px 10px;
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
// height: 40px;
flex-direction: row;
align-items: center;
// border-bottom: solid 1px #DDDDDD;
}
.uni-combox__label {
font-size: 16px;
line-height: 22px;
padding-right: 10px;
color: #999999;
}
.uni-combox__input-box {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
}
.uni-combox__input {
flex: 1;
font-size: 14px;
height: 22px;
line-height: 22px;
}
.uni-combox__input-plac {
font-size: 14px;
color: #999;
}
.uni-combox__selector {
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
position: absolute;
top: calc(100% + 12px);
left: 0;
width: 100%;
background-color: #FFFFFF;
border: 1px solid #EBEEF5;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 2;
padding: 4px 0;
}
.uni-combox__selector-scroll {
/* #ifndef APP-NVUE */
max-height: 200px;
box-sizing: border-box;
/* #endif */
}
.uni-combox__selector-empty,
.uni-combox__selector-item {
/* #ifndef APP-NVUE */
display: flex;
cursor: pointer;
/* #endif */
line-height: 36px;
font-size: 14px;
text-align: center;
// border-bottom: solid 1px #DDDDDD;
padding: 0px 10px;
}
.uni-combox__selector-item:hover {
background-color: #f9f9f9;
}
.uni-combox__selector-empty:last-child,
.uni-combox__selector-item:last-child {
/* #ifndef APP-NVUE */
border-bottom: none;
/* #endif */
}
// picker 弹出层通用的指示小三角
.uni-popper__arrow,
.uni-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
}
.uni-popper__arrow {
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
top: -6px;
left: 10%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #EBEEF5;
}
.uni-popper__arrow::after {
content: " ";
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
.uni-combox__no-border {
border: none;
}
</style>

View File

@@ -0,0 +1,88 @@
{
"id": "uni-combox",
"displayName": "uni-combox 组合框",
"version": "1.0.2",
"description": "可以选择也可以输入的表单项 ",
"keywords": [
"uni-ui",
"uniui",
"combox",
"组合框",
"select"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [
"uni-scss",
"uni-icons"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
## Combox 组合框
> **组件名:uni-combox**
> 代码块: `uCombox`
组合框组件。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-combox)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

View File

@@ -0,0 +1,30 @@
## 1.2.5(2025-04-14)
- 修复 filterShow 导致的运行报错
## 1.2.4(2024-09-21)
- 新增 支持控制显示位数 默认显示2位
## 1.2.3(2024-02-20)
- 新增 支持控制小时,分钟的显隐:showHour showMinute
## 1.2.2(2022-01-19)
- 修复 在微信小程序中样式不生效的bug
## 1.2.1(2022-01-18)
- 新增 update 方法 ,在动态更新时间后,刷新组件
## 1.2.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-countdown](https://uniapp.dcloud.io/component/uniui/uni-countdown)
## 1.1.3(2021-10-18)
- 重构
- 新增 font-size 支持自定义字体大小
## 1.1.2(2021-08-24)
- 新增 支持国际化
## 1.1.1(2021-07-30)
- 优化 vue3下小程序事件警告的问题
## 1.1.0(2021-07-30)
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.0.5(2021-06-18)
- 修复 uni-countdown 重复赋值跳两秒的 bug
## 1.0.4(2021-05-12)
- 新增 组件示例地址
## 1.0.3(2021-05-08)
- 修复 uni-countdown 不能控制倒计时的 bug
## 1.0.2(2021-02-04)
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,6 @@
{
"uni-countdown.day": "day",
"uni-countdown.h": "h",
"uni-countdown.m": "m",
"uni-countdown.s": "s"
}

View File

@@ -0,0 +1,8 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
import zhHant from './zh-Hant.json'
export default {
en,
'zh-Hans': zhHans,
'zh-Hant': zhHant
}

View File

@@ -0,0 +1,6 @@
{
"uni-countdown.day": "天",
"uni-countdown.h": "时",
"uni-countdown.m": "分",
"uni-countdown.s": "秒"
}

View File

@@ -0,0 +1,6 @@
{
"uni-countdown.day": "天",
"uni-countdown.h": "時",
"uni-countdown.m": "分",
"uni-countdown.s": "秒"
}

View File

@@ -0,0 +1,278 @@
<template>
<view class="uni-countdown">
<text v-if="showDay" :style="[timeStyle]" class="uni-countdown__number">{{ d }}</text>
<text v-if="showDay" :style="[splitorStyle]" class="uni-countdown__splitor">{{dayText}}</text>
<text v-if="showHour" :style="[timeStyle]" class="uni-countdown__number">{{ h }}</text>
<text v-if="showHour" :style="[splitorStyle]" class="uni-countdown__splitor">{{ showColon ? ':' : hourText }}</text>
<text v-if="showMinute" :style="[timeStyle]" class="uni-countdown__number">{{ i }}</text>
<text v-if="showMinute" :style="[splitorStyle]" class="uni-countdown__splitor">{{ showColon ? ':' : minuteText }}</text>
<text :style="[timeStyle]" class="uni-countdown__number">{{ s }}</text>
<text v-if="!showColon" :style="[splitorStyle]" class="uni-countdown__splitor">{{secondText}}</text>
</view>
</template>
<script>
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const {
t
} = initVueI18n(messages)
/**
* Countdown 倒计时
* @description 倒计时组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=25
* @property {String} backgroundColor 背景色
* @property {String} color 文字颜色
* @property {Number} day 天数
* @property {Number} hour 小时
* @property {Number} minute 分钟
* @property {Number} second 秒
* @property {Number} timestamp 时间戳
* @property {Boolean} showDay = [true|false] 是否显示天数
* @property {Boolean} showHour = [true|false] 是否显示小时
* @property {Boolean} showMinute = [true|false] 是否显示分钟
* @property {Boolean} show-colon = [true|false] 是否以冒号为分隔符
* @property {String} splitorColor 分割符号颜色
* @event {Function} timeup 倒计时时间到触发事件
* @example <uni-countdown :day="1" :hour="1" :minute="12" :second="40"></uni-countdown>
*/
export default {
name: 'UniCountdown',
emits: ['timeup'],
props: {
showDay: {
type: Boolean,
default: true
},
showHour: {
type: Boolean,
default: true
},
showMinute: {
type: Boolean,
default: true
},
showColon: {
type: Boolean,
default: true
},
start: {
type: Boolean,
default: true
},
backgroundColor: {
type: String,
default: ''
},
color: {
type: String,
default: '#333'
},
fontSize: {
type: Number,
default: 14
},
splitorColor: {
type: String,
default: '#333'
},
day: {
type: Number,
default: 0
},
hour: {
type: Number,
default: 0
},
minute: {
type: Number,
default: 0
},
second: {
type: Number,
default: 0
},
timestamp: {
type: Number,
default: 0
},
filterShow : {
type:Object,
default () {
return {}
}
}
},
data() {
return {
timer: null,
syncFlag: false,
d: '00',
h: '00',
i: '00',
s: '00',
leftTime: 0,
seconds: 0
}
},
computed: {
dayText() {
return t("uni-countdown.day")
},
hourText(val) {
return t("uni-countdown.h")
},
minuteText(val) {
return t("uni-countdown.m")
},
secondText(val) {
return t("uni-countdown.s")
},
timeStyle() {
const {
color,
backgroundColor,
fontSize
} = this
return {
color,
backgroundColor,
fontSize: `${fontSize}px`,
width: `${fontSize * 22 / 14}px`, // 按字体大小为 14px 时的比例缩放
lineHeight: `${fontSize * 20 / 14}px`,
borderRadius: `${fontSize * 3 / 14}px`,
}
},
splitorStyle() {
const { splitorColor, fontSize, backgroundColor } = this
return {
color: splitorColor,
fontSize: `${fontSize * 12 / 14}px`,
margin: backgroundColor ? `${fontSize * 4 / 14}px` : ''
}
}
},
watch: {
day(val) {
this.changeFlag()
},
hour(val) {
this.changeFlag()
},
minute(val) {
this.changeFlag()
},
second(val) {
this.changeFlag()
},
start: {
immediate: true,
handler(newVal, oldVal) {
if (newVal) {
this.startData();
} else {
if (!oldVal) return
clearInterval(this.timer)
}
}
}
},
created: function(e) {
this.seconds = this.toSeconds(this.timestamp, this.day, this.hour, this.minute, this.second)
this.countDown()
},
// #ifndef VUE3
destroyed() {
clearInterval(this.timer)
},
// #endif
// #ifdef VUE3
unmounted() {
clearInterval(this.timer)
},
// #endif
methods: {
toSeconds(timestamp, day, hours, minutes, seconds) {
if (timestamp) {
return timestamp - parseInt(new Date().getTime() / 1000, 10)
}
return day * 60 * 60 * 24 + hours * 60 * 60 + minutes * 60 + seconds
},
timeUp() {
clearInterval(this.timer)
this.$emit('timeup')
},
countDown() {
let seconds = this.seconds
let [day, hour, minute, second] = [0, 0, 0, 0]
if (seconds > 0) {
day = Math.floor(seconds / (60 * 60 * 24))
hour = Math.floor(seconds / (60 * 60)) - (day * 24)
minute = Math.floor(seconds / 60) - (day * 24 * 60) - (hour * 60)
second = Math.floor(seconds) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60)
} else {
this.timeUp()
}
this.d = String(day).padStart(this.validFilterShow(this.filterShow.d), '0')
this.h = String(hour).padStart(this.validFilterShow(this.filterShow.h), '0')
this.i = String(minute).padStart(this.validFilterShow(this.filterShow.m), '0')
this.s = String(second).padStart(this.validFilterShow(this.filterShow.s), '0')
},
validFilterShow(filter){
return (filter && filter > 0) ? filter : 2;
},
startData() {
this.seconds = this.toSeconds(this.timestamp, this.day, this.hour, this.minute, this.second)
if (this.seconds <= 0) {
this.seconds = this.toSeconds(0, 0, 0, 0, 0)
this.countDown()
return
}
clearInterval(this.timer)
this.countDown()
this.timer = setInterval(() => {
this.seconds--
if (this.seconds < 0) {
this.timeUp()
return
}
this.countDown()
}, 1000)
},
update(){
this.startData();
},
changeFlag() {
if (!this.syncFlag) {
this.seconds = this.toSeconds(this.timestamp, this.day, this.hour, this.minute, this.second)
this.startData();
this.syncFlag = true;
}
}
}
}
</script>
<style lang="scss" scoped>
$font-size: 14px;
.uni-countdown {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
&__splitor {
margin: 0 2px;
font-size: $font-size;
color: #333;
}
&__number {
border-radius: 3px;
text-align: center;
font-size: $font-size;
}
}
</style>

View File

@@ -0,0 +1,86 @@
{
"id": "uni-countdown",
"displayName": "uni-countdown 倒计时",
"version": "1.2.5",
"description": "CountDown 倒计时组件",
"keywords": [
"uni-ui",
"uniui",
"countdown",
"倒计时"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": ["uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-harmony": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
## CountDown 倒计时
> **组件名:uni-countdown**
> 代码块: `uCountDown`
倒计时组件。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-countdown)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

View File

@@ -0,0 +1,51 @@
## 1.0.6(2024-10-22)
- 新增 当 multiple 为 false 且传递的 value 为 数组时,使用数组第一项用作反显
## 1.0.5(2024-03-20)
- 修复 单选模式下选中样式不生效的bug
## 1.0.4(2024-01-27)
- 修复 修复错别字chagne为change
## 1.0.3(2022-09-16)
- 可以使用 uni-scss 控制主题色
## 1.0.2(2022-06-30)
- 优化 在 uni-forms 中的依赖注入方式
## 1.0.1(2022-02-07)
- 修复 multiple 为 true 时,v-model 的值为 null 报错的 bug
## 1.0.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-checkbox](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox)
## 0.2.5(2021-08-23)
- 修复 在uni-forms中 modelValue 中不存在当前字段,当前字段必填写也不参与校验的问题
## 0.2.4(2021-08-17)
- 修复 单选 list 模式下 ,icon 为 left 时,选中图标不显示的问题
## 0.2.3(2021-08-11)
- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题
## 0.2.2(2021-07-30)
- 优化 在uni-forms组件,与label不对齐的问题
## 0.2.1(2021-07-27)
- 修复 单选默认值为0不能选中的Bug
## 0.2.0(2021-07-13)
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 0.1.11(2021-07-06)
- 优化 删除无用日志
## 0.1.10(2021-07-05)
- 修复 由 0.1.9 引起的非 nvue 端图标不显示的问题
## 0.1.9(2021-07-05)
- 修复 nvue 黑框样式问题
## 0.1.8(2021-06-28)
- 修复 selectedTextColor 属性不生效的Bug
## 0.1.7(2021-06-02)
- 新增 map 属性,可以方便映射text/value属性
## 0.1.6(2021-05-26)
- 修复 不关联服务空间的情况下组件报错的Bug
## 0.1.5(2021-05-12)
- 新增 组件示例地址
## 0.1.4(2021-04-09)
- 修复 nvue 下无法选中的问题
## 0.1.3(2021-03-22)
- 新增 disabled属性
## 0.1.2(2021-02-24)
- 优化 默认颜色显示
## 0.1.1(2021-02-24)
- 新增 支持nvue
## 0.1.0(2021-02-18)
- “暂无数据”显示居中

View File

@@ -0,0 +1,316 @@
const events = {
load: 'load',
error: 'error'
}
const pageMode = {
add: 'add',
replace: 'replace'
}
const attrs = [
'pageCurrent',
'pageSize',
'collection',
'action',
'field',
'getcount',
'orderby',
'where'
]
export default {
data() {
return {
loading: false,
listData: this.getone ? {} : [],
paginationInternal: {
current: this.pageCurrent,
size: this.pageSize,
count: 0
},
errorMessage: ''
}
},
created() {
let db = null;
let dbCmd = null;
if(this.collection){
this.db = uniCloud.database();
this.dbCmd = this.db.command;
}
this._isEnded = false
this.$watch(() => {
let al = []
attrs.forEach(key => {
al.push(this[key])
})
return al
}, (newValue, oldValue) => {
this.paginationInternal.pageSize = this.pageSize
let needReset = false
for (let i = 2; i < newValue.length; i++) {
if (newValue[i] != oldValue[i]) {
needReset = true
break
}
}
if (needReset) {
this.clear()
this.reset()
}
if (newValue[0] != oldValue[0]) {
this.paginationInternal.current = this.pageCurrent
}
this._execLoadData()
})
// #ifdef H5
if (process.env.NODE_ENV === 'development') {
this._debugDataList = []
if (!window.unidev) {
window.unidev = {
clientDB: {
data: []
}
}
}
unidev.clientDB.data.push(this._debugDataList)
}
// #endif
// #ifdef MP-TOUTIAO
let changeName
let events = this.$scope.dataset.eventOpts
for (let i = 0; i < events.length; i++) {
let event = events[i]
if (event[0].includes('^load')) {
changeName = event[1][0][0]
}
}
if (changeName) {
let parent = this.$parent
let maxDepth = 16
this._changeDataFunction = null
while (parent && maxDepth > 0) {
let fun = parent[changeName]
if (fun && typeof fun === 'function') {
this._changeDataFunction = fun
maxDepth = 0
break
}
parent = parent.$parent
maxDepth--;
}
}
// #endif
// if (!this.manual) {
// this.loadData()
// }
},
// #ifdef H5
beforeDestroy() {
if (process.env.NODE_ENV === 'development' && window.unidev) {
let cd = this._debugDataList
let dl = unidev.clientDB.data
for (let i = dl.length - 1; i >= 0; i--) {
if (dl[i] === cd) {
dl.splice(i, 1)
break
}
}
}
},
// #endif
methods: {
loadData(args1, args2) {
let callback = null
if (typeof args1 === 'object') {
if (args1.clear) {
this.clear()
this.reset()
}
if (args1.current !== undefined) {
this.paginationInternal.current = args1.current
}
if (typeof args2 === 'function') {
callback = args2
}
} else if (typeof args1 === 'function') {
callback = args1
}
this._execLoadData(callback)
},
loadMore() {
if (this._isEnded) {
return
}
this._execLoadData()
},
refresh() {
this.clear()
this._execLoadData()
},
clear() {
this._isEnded = false
this.listData = []
},
reset() {
this.paginationInternal.current = 1
},
remove(id, {
action,
callback,
confirmTitle,
confirmContent
} = {}) {
if (!id || !id.length) {
return
}
uni.showModal({
title: confirmTitle || '提示',
content: confirmContent || '是否删除该数据',
showCancel: true,
success: (res) => {
if (!res.confirm) {
return
}
this._execRemove(id, action, callback)
}
})
},
_execLoadData(callback) {
if (this.loading) {
return
}
this.loading = true
this.errorMessage = ''
this._getExec().then((res) => {
this.loading = false
const {
data,
count
} = res.result
this._isEnded = data.length < this.pageSize
callback && callback(data, this._isEnded)
this._dispatchEvent(events.load, data)
if (this.getone) {
this.listData = data.length ? data[0] : undefined
} else if (this.pageData === pageMode.add) {
this.listData.push(...data)
if (this.listData.length) {
this.paginationInternal.current++
}
} else if (this.pageData === pageMode.replace) {
this.listData = data
this.paginationInternal.count = count
}
// #ifdef H5
if (process.env.NODE_ENV === 'development') {
this._debugDataList.length = 0
this._debugDataList.push(...JSON.parse(JSON.stringify(this.listData)))
}
// #endif
}).catch((err) => {
this.loading = false
this.errorMessage = err
callback && callback()
this.$emit(events.error, err)
})
},
_getExec() {
let exec = this.db
if (this.action) {
exec = exec.action(this.action)
}
exec = exec.collection(this.collection)
if (!(!this.where || !Object.keys(this.where).length)) {
exec = exec.where(this.where)
}
if (this.field) {
exec = exec.field(this.field)
}
if (this.orderby) {
exec = exec.orderBy(this.orderby)
}
const {
current,
size
} = this.paginationInternal
exec = exec.skip(size * (current - 1)).limit(size).get({
getCount: this.getcount
})
return exec
},
_execRemove(id, action, callback) {
if (!this.collection || !id) {
return
}
const ids = Array.isArray(id) ? id : [id]
if (!ids.length) {
return
}
uni.showLoading({
mask: true
})
let exec = this.db
if (action) {
exec = exec.action(action)
}
exec.collection(this.collection).where({
_id: dbCmd.in(ids)
}).remove().then((res) => {
callback && callback(res.result)
if (this.pageData === pageMode.replace) {
this.refresh()
} else {
this.removeData(ids)
}
}).catch((err) => {
uni.showModal({
content: err.message,
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
removeData(ids) {
let il = ids.slice(0)
let dl = this.listData
for (let i = dl.length - 1; i >= 0; i--) {
let index = il.indexOf(dl[i]._id)
if (index >= 0) {
dl.splice(i, 1)
il.splice(index, 1)
}
}
},
_dispatchEvent(type, data) {
if (this._changeDataFunction) {
this._changeDataFunction(data, this._isEnded)
} else {
this.$emit(type, data, this._isEnded)
}
}
}
}

View File

@@ -0,0 +1,853 @@
<template>
<view class="uni-data-checklist" :style="{'margin-top':isTop+'px'}">
<template v-if="!isLocal">
<view class="uni-data-loading">
<uni-load-more v-if="!mixinDatacomErrorMessage" status="loading" iconType="snow" :iconSize="18"
:content-text="contentText"></uni-load-more>
<text v-else>{{mixinDatacomErrorMessage}}</text>
</view>
</template>
<template v-else>
<checkbox-group v-if="multiple" class="checklist-group" :class="{'is-list':mode==='list' || wrap}"
@change="change">
<label class="checklist-box"
:class="['is--'+mode,item.selected?'is-checked':'',(disabled || !!item.disabled)?'is-disable':'',index!==0&&mode==='list'?'is-list-border':'']"
:style="item.styleBackgroud" v-for="(item,index) in dataList" :key="index">
<checkbox class="hidden" hidden :disabled="disabled || !!item.disabled" :value="item[map.value]+''"
:checked="item.selected" />
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')"
class="checkbox__inner" :style="item.styleIcon">
<view class="checkbox__inner-icon"></view>
</view>
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}">
<text class="checklist-text" :style="item.styleIconText">{{item[map.text]}}</text>
<view v-if="mode === 'list' && icon === 'right'" class="checkobx__list" :style="item.styleBackgroud"></view>
</view>
</label>
</checkbox-group>
<radio-group v-else class="checklist-group" :class="{'is-list':mode==='list','is-wrap':wrap}" @change="change">
<label class="checklist-box"
:class="['is--'+mode,item.selected?'is-checked':'',(disabled || !!item.disabled)?'is-disable':'',index!==0&&mode==='list'?'is-list-border':'']"
:style="item.styleBackgroud" v-for="(item,index) in dataList" :key="index">
<radio class="hidden" hidden :disabled="disabled || item.disabled" :value="item[map.value]+''"
:checked="item.selected" />
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')" class="radio__inner"
:style="item.styleBackgroud">
<view class="radio__inner-icon" :style="item.styleIcon"></view>
</view>
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}">
<text class="checklist-text" :style="item.styleIconText">{{item[map.text]}}</text>
<view v-if="mode === 'list' && icon === 'right'" :style="item.styleRightIcon" class="checkobx__list"></view>
</view>
</label>
</radio-group>
</template>
</view>
</template>
<script>
/**
* DataChecklist 数据选择器
* @description 通过数据渲染 checkbox 和 radio
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
* @property {String} mode = [default| list | button | tag] 显示模式
* @value default 默认横排模式
* @value list 列表模式
* @value button 按钮模式
* @value tag 标签模式
* @property {Boolean} multiple = [true|false] 是否多选
* @property {Array|String|Number} value 默认值
* @property {Array} localdata 本地数据 ,格式 [{text:'',value:''}]
* @property {Number|String} min 最小选择个数 ,multiple为true时生效
* @property {Number|String} max 最大选择个数 ,multiple为true时生效
* @property {Boolean} wrap 是否换行显示
* @property {String} icon = [left|right] list 列表模式下icon显示位置
* @property {Boolean} selectedColor 选中颜色
* @property {Boolean} emptyText 没有数据时显示的文字 ,本地数据无效
* @property {Boolean} selectedTextColor 选中文本颜色,如不填写则自动显示
* @property {Object} map 字段映射, 默认 map={text:'text',value:'value'}
* @value left 左侧显示
* @value right 右侧显示
* @event {Function} change 选中发生变化触发
*/
export default {
name: 'uniDataChecklist',
mixins: [uniCloud.mixinDatacom || {}],
emits: ['input', 'update:modelValue', 'change'],
props: {
mode: {
type: String,
default: 'default'
},
multiple: {
type: Boolean,
default: false
},
value: {
type: [Array, String, Number],
default () {
return ''
}
},
// TODO vue3
modelValue: {
type: [Array, String, Number],
default () {
return '';
}
},
localdata: {
type: Array,
default () {
return []
}
},
min: {
type: [Number, String],
default: ''
},
max: {
type: [Number, String],
default: ''
},
wrap: {
type: Boolean,
default: false
},
icon: {
type: String,
default: 'left'
},
selectedColor: {
type: String,
default: ''
},
selectedTextColor: {
type: String,
default: ''
},
emptyText: {
type: String,
default: '暂无数据'
},
disabled: {
type: Boolean,
default: false
},
map: {
type: Object,
default () {
return {
text: 'text',
value: 'value'
}
}
}
},
watch: {
localdata: {
handler(newVal) {
this.range = newVal
this.dataList = this.getDataList(this.getSelectedValue(newVal))
},
deep: true
},
mixinDatacomResData(newVal) {
this.range = newVal
this.dataList = this.getDataList(this.getSelectedValue(newVal))
},
value(newVal) {
this.dataList = this.getDataList(newVal)
// fix by mehaotian is_reset 在 uni-forms 中定义
// if(!this.is_reset){
// this.is_reset = false
// this.formItem && this.formItem.setValue(newVal)
// }
},
modelValue(newVal) {
this.dataList = this.getDataList(newVal);
// if(!this.is_reset){
// this.is_reset = false
// this.formItem && this.formItem.setValue(newVal)
// }
}
},
data() {
return {
dataList: [],
range: [],
contentText: {
contentdown: '查看更多',
contentrefresh: '加载中',
contentnomore: '没有更多'
},
isLocal: true,
styles: {
selectedColor: '#2979ff',
selectedTextColor: '#666',
},
isTop: 0
};
},
computed: {
dataValue() {
if (this.value === '') return this.modelValue
if (this.modelValue === '') return this.value
return this.value
}
},
created() {
// this.form = this.getForm('uniForms')
// this.formItem = this.getForm('uniFormsItem')
// this.formItem && this.formItem.setValue(this.value)
// if (this.formItem) {
// this.isTop = 6
// if (this.formItem.name) {
// // 如果存在name添加默认值,否则formData 中不存在这个字段不校验
// if(!this.is_reset){
// this.is_reset = false
// this.formItem.setValue(this.dataValue)
// }
// this.rename = this.formItem.name
// this.form.inputChildrens.push(this)
// }
// }
if (this.localdata && this.localdata.length !== 0) {
this.isLocal = true
this.range = this.localdata
this.dataList = this.getDataList(this.getSelectedValue(this.range))
} else {
if (this.collection) {
this.isLocal = false
this.loadData()
}
}
},
methods: {
loadData() {
this.mixinDatacomGet().then(res => {
this.mixinDatacomResData = res.result.data
if (this.mixinDatacomResData.length === 0) {
this.isLocal = false
this.mixinDatacomErrorMessage = this.emptyText
} else {
this.isLocal = true
}
}).catch(err => {
this.mixinDatacomErrorMessage = err.message
})
},
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
change(e) {
const values = e.detail.value
let detail = {
value: [],
data: []
}
if (this.multiple) {
this.range.forEach(item => {
if (values.includes(item[this.map.value] + '')) {
detail.value.push(item[this.map.value])
detail.data.push(item)
}
})
} else {
const range = this.range.find(item => (item[this.map.value] + '') === values)
if (range) {
detail = {
value: range[this.map.value],
data: range
}
}
}
// this.formItem && this.formItem.setValue(detail.value)
// TODO 兼容 vue2
this.$emit('input', detail.value);
// // TOTO 兼容 vue3
this.$emit('update:modelValue', detail.value);
this.$emit('change', {
detail
})
if (this.multiple) {
// 如果 v-model 没有绑定 ,则走内部逻辑
// if (this.value.length === 0) {
this.dataList = this.getDataList(detail.value, true)
// }
} else {
this.dataList = this.getDataList(detail.value)
}
},
/**
* 获取渲染的新数组
* @param {Object} value 选中内容
*/
getDataList(value) {
// 解除引用关系,破坏原引用关系,避免污染源数据
let dataList = JSON.parse(JSON.stringify(this.range))
let list = []
if (this.multiple) {
if (!Array.isArray(value)) {
value = []
}
} else {
if (Array.isArray(value) && value.length) {
value = value[0]
}
}
dataList.forEach((item, index) => {
item.disabled = item.disable || item.disabled || false
if (this.multiple) {
if (value.length > 0) {
let have = value.find(val => val === item[this.map.value])
item.selected = have !== undefined
} else {
item.selected = false
}
} else {
item.selected = value === item[this.map.value]
}
list.push(item)
})
return this.setRange(list)
},
/**
* 处理最大最小值
* @param {Object} list
*/
setRange(list) {
let selectList = list.filter(item => item.selected)
let min = Number(this.min) || 0
let max = Number(this.max) || ''
list.forEach((item, index) => {
if (this.multiple) {
if (selectList.length <= min) {
let have = selectList.find(val => val[this.map.value] === item[this.map.value])
if (have !== undefined) {
item.disabled = true
}
}
if (selectList.length >= max && max !== '') {
let have = selectList.find(val => val[this.map.value] === item[this.map.value])
if (have === undefined) {
item.disabled = true
}
}
}
this.setStyles(item, index)
list[index] = item
})
return list
},
/**
* 设置 class
* @param {Object} item
* @param {Object} index
*/
setStyles(item, index) {
// 设置自定义样式
item.styleBackgroud = this.setStyleBackgroud(item)
item.styleIcon = this.setStyleIcon(item)
item.styleIconText = this.setStyleIconText(item)
item.styleRightIcon = this.setStyleRightIcon(item)
},
/**
* 获取选中值
* @param {Object} range
*/
getSelectedValue(range) {
if (!this.multiple) return this.dataValue
let selectedArr = []
range.forEach((item) => {
if (item.selected) {
selectedArr.push(item[this.map.value])
}
})
return this.dataValue.length > 0 ? this.dataValue : selectedArr
},
/**
* 设置背景样式
*/
setStyleBackgroud(item) {
let styles = {}
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
if (this.selectedColor) {
if (this.mode !== 'list') {
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
}
if (this.mode === 'tag') {
styles['background-color'] = item.selected ? selectedColor : '#f5f5f5'
}
}
let classles = ''
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleIcon(item) {
let styles = {}
let classles = ''
if (this.selectedColor) {
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
styles['background-color'] = item.selected ? selectedColor : '#fff'
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
if (!item.selected && item.disabled) {
styles['background-color'] = '#F2F6FC'
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
}
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleIconText(item) {
let styles = {}
let classles = ''
if (this.selectedColor) {
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
if (this.mode === 'tag') {
styles.color = item.selected ? (this.selectedTextColor ? this.selectedTextColor : '#fff') : '#666'
} else {
styles.color = item.selected ? (this.selectedTextColor ? this.selectedTextColor : selectedColor) : '#666'
}
if (!item.selected && item.disabled) {
styles.color = '#999'
}
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleRightIcon(item) {
let styles = {}
let classles = ''
if (this.mode === 'list') {
styles['border-color'] = item.selected ? this.styles.selectedColor : '#DCDFE6'
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
}
}
}
</script>
<style lang="scss">
$uni-primary: #2979ff !default;
$border-color: #DCDFE6;
$disable: 0.4;
@mixin flex {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
}
.uni-data-loading {
@include flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 36px;
padding-left: 10px;
color: #999;
}
.uni-data-checklist {
position: relative;
z-index: 0;
flex: 1;
// 多选样式
.checklist-group {
@include flex;
flex-direction: row;
flex-wrap: wrap;
&.is-list {
flex-direction: column;
}
.checklist-box {
@include flex;
flex-direction: row;
align-items: center;
position: relative;
margin: 5px 0;
margin-right: 25px;
.hidden {
position: absolute;
opacity: 0;
}
// 文字样式
.checklist-content {
@include flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: space-between;
.checklist-text {
font-size: 14px;
color: #666;
margin-left: 5px;
line-height: 14px;
}
.checkobx__list {
border-right-width: 1px;
border-right-color: #007aff;
border-right-style: solid;
border-bottom-width: 1px;
border-bottom-color: #007aff;
border-bottom-style: solid;
height: 12px;
width: 6px;
left: -5px;
transform-origin: center;
transform: rotate(45deg);
opacity: 0;
}
}
// 多选样式
.checkbox__inner {
/* #ifndef APP-NVUE */
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
position: relative;
width: 16px;
height: 16px;
border: 1px solid $border-color;
border-radius: 4px;
background-color: #fff;
z-index: 1;
.checkbox__inner-icon {
position: absolute;
/* #ifdef APP-NVUE */
top: 2px;
/* #endif */
/* #ifndef APP-NVUE */
top: 1px;
/* #endif */
left: 5px;
height: 8px;
width: 4px;
border-right-width: 1px;
border-right-color: #fff;
border-right-style: solid;
border-bottom-width: 1px;
border-bottom-color: #fff;
border-bottom-style: solid;
opacity: 0;
transform-origin: center;
transform: rotate(40deg);
}
}
// 单选样式
.radio__inner {
@include flex;
/* #ifndef APP-NVUE */
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
justify-content: center;
align-items: center;
position: relative;
width: 16px;
height: 16px;
border: 1px solid $border-color;
border-radius: 16px;
background-color: #fff;
z-index: 1;
.radio__inner-icon {
width: 8px;
height: 8px;
border-radius: 10px;
opacity: 0;
}
}
// 默认样式
&.is--default {
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.radio__inner {
background-color: #F2F6FC;
border-color: $border-color;
}
.checklist-text {
color: #999;
}
}
// 选中
&.is-checked {
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
// 选中禁用
&.is-disable {
.checkbox__inner {
opacity: $disable;
}
.checklist-text {
opacity: $disable;
}
.radio__inner {
opacity: $disable;
}
}
}
}
// 按钮样式
&.is--button {
margin-right: 10px;
padding: 5px 10px;
border: 1px $border-color solid;
border-radius: 3px;
transition: border-color 0.2s;
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
border: 1px #eee solid;
opacity: $disable;
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.radio__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.checklist-text {
color: #999;
}
}
&.is-checked {
border-color: $uni-primary;
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
// 选中禁用
&.is-disable {
opacity: $disable;
}
}
}
// 标签样式
&.is--tag {
margin-right: 10px;
padding: 5px 10px;
border: 1px $border-color solid;
border-radius: 3px;
background-color: #f5f5f5;
.checklist-text {
margin: 0;
color: #666;
}
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
opacity: $disable;
}
&.is-checked {
background-color: $uni-primary;
border-color: $uni-primary;
.checklist-text {
color: #fff;
}
}
}
// 列表样式
&.is--list {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
padding: 10px 15px;
padding-left: 0;
margin: 0;
&.is-list-border {
border-top: 1px #eee solid;
}
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.checklist-text {
color: #999;
}
}
&.is-checked {
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
.checklist-content {
.checkobx__list {
opacity: 1;
border-color: $uni-primary;
}
}
// 选中禁用
&.is-disable {
.checkbox__inner {
opacity: $disable;
}
.checklist-text {
opacity: $disable;
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,87 @@
{
"id": "uni-data-checkbox",
"displayName": "uni-data-checkbox 数据选择器",
"version": "1.0.6",
"description": "通过数据驱动的单选框和复选框",
"keywords": [
"uni-ui",
"checkbox",
"单选",
"多选",
"单选多选"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "^3.1.1"
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": ["uni-load-more","uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-harmony": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
## DataCheckbox 数据驱动的单选复选框
> **组件名:uni-data-checkbox**
> 代码块: `uDataCheckbox`
本组件是基于uni-app基础组件checkbox的封装。本组件要解决问题包括:
1. 数据绑定型组件:给本组件绑定一个data,会自动渲染一组候选内容。再以往,开发者需要编写不少代码实现类似功能
2. 自动的表单校验:组件绑定了data,且符合[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)组件的表单校验规范,搭配使用会自动实现表单校验
3. 本组件合并了单选多选
4. 本组件有若干风格选择,如普通的单选多选框、并列button风格、tag风格。开发者可以快速选择需要的风格。但作为一个封装组件,样式代码虽然不用自己写了,却会牺牲一定的样式自定义性
在uniCloud开发中,`DB Schema`中配置了enum枚举等类型后,在web控制台的[自动生成表单](https://uniapp.dcloud.io/uniCloud/schema?id=autocode)功能中,会自动生成``uni-data-checkbox``组件并绑定好data
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

View File

@@ -0,0 +1,79 @@
## 2.0.2(2025-04-14)
- 修复 在readonly属性为true时选项匹配错误的问题
## 2.0.0(2023-12-14)
- 新增 支持 uni-app-x
## 1.1.2(2023-04-11)
- 修复 更改 modelValue 报错的 bug
- 修复 v-for 未使用 key 值控制台 warning
## 1.1.1(2023-02-21)
- 修复代码合并时引发 value 属性为空时不渲染数据的问题
## 1.1.0(2023-02-15)
- 修复 localdata 不支持动态更新的bug
## 1.0.9(2023-02-15)
- 修复 localdata 不支持动态更新的bug
## 1.0.8(2022-09-16)
- 可以使用 uni-scss 控制主题色
## 1.0.7(2022-07-06)
- 优化 pc端图标位置不正确的问题
## 1.0.6(2022-07-05)
- 优化 显示样式
## 1.0.5(2022-07-04)
- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug
## 1.0.4(2022-04-19)
- 修复 字节小程序 本地数据无法选择下一级的Bug
## 1.0.3(2022-02-25)
- 修复 nvue 不支持的 v-show 的 bug
## 1.0.2(2022-02-25)
- 修复 条件编译 nvue 不支持的 css 样式
## 1.0.1(2021-11-23)
- 修复 由上个版本引发的map、v-model等属性不生效的bug
## 1.0.0(2021-11-19)
- 优化 组件 UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-picker](https://uniapp.dcloud.io/component/uniui/uni-data-picker)
## 0.4.9(2021-10-28)
- 修复 VUE2 v-model 概率无效的 bug
## 0.4.8(2021-10-27)
- 修复 v-model 概率无效的 bug
## 0.4.7(2021-10-25)
- 新增 属性 spaceInfo 服务空间配置 HBuilderX 3.2.11+
- 修复 树型 uniCloud 数据类型为 int 时报错的 bug
## 0.4.6(2021-10-19)
- 修复 非 VUE3 v-model 为 0 时无法选中的 bug
## 0.4.5(2021-09-26)
- 新增 清除已选项的功能(通过 clearIcon 属性配置是否显示按钮),同时提供 clear 方法以供调用,二者等效
- 修复 readonly 为 true 时报错的 bug
## 0.4.4(2021-09-26)
- 修复 上一版本造成的 map 属性失效的 bug
- 新增 ellipsis 属性,支持配置 tab 选项长度过长时是否自动省略
## 0.4.3(2021-09-24)
- 修复 某些情况下级联未触发的 bug
## 0.4.2(2021-09-23)
- 新增 提供 show 和 hide 方法,开发者可以通过 ref 调用
- 新增 选项内容过长自动添加省略号
## 0.4.1(2021-09-15)
- 新增 map 属性 字段映射,将 text/value 映射到数据中的其他字段
## 0.4.0(2021-07-13)
- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 0.3.5(2021-06-04)
- 修复 无法加载云端数据的问题
## 0.3.4(2021-05-28)
- 修复 v-model 无效问题
- 修复 loaddata 为空数据组时加载时间过长问题
- 修复 上个版本引出的本地数据无法选择带有 children 的 2 级节点
## 0.3.3(2021-05-12)
- 新增 组件示例地址
## 0.3.2(2021-04-22)
- 修复 非树形数据有 where 属性查询报错的问题
## 0.3.1(2021-04-15)
- 修复 本地数据概率无法回显时问题
## 0.3.0(2021-04-07)
- 新增 支持云端非树形表结构数据
- 修复 根节点 parent_field 字段等于 null 时选择界面错乱问题
## 0.2.0(2021-03-15)
- 修复 nodeclick、popupopened、popupclosed 事件无法触发的问题
## 0.1.9(2021-03-09)
- 修复 微信小程序某些情况下无法选择的问题
## 0.1.8(2021-02-05)
- 优化 部分样式在 nvue 上的兼容表现
## 0.1.7(2021-02-05)
- 调整为 uni_modules 目录规范

View File

@@ -0,0 +1,45 @@
// #ifdef H5
export default {
name: 'Keypress',
props: {
disable: {
type: Boolean,
default: false
}
},
mounted () {
const keyNames = {
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
space: [' ', 'Spacebar'],
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete', 'Del']
}
const listener = ($event) => {
if (this.disable) {
return
}
const keyName = Object.keys(keyNames).find(key => {
const keyName = $event.key
const value = keyNames[key]
return value === keyName || (Array.isArray(value) && value.includes(keyName))
})
if (keyName) {
// 避免和其他按键事件冲突
setTimeout(() => {
this.$emit(keyName, {})
}, 0)
}
}
document.addEventListener('keyup', listener)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keyup', listener)
})
},
render: () => {}
}
// #endif

View File

@@ -0,0 +1,380 @@
<template>
<view class="uni-data-tree">
<view class="uni-data-tree-input" @click="handleInput">
<slot :data="selectedPaths" :error="error">
<view class="input-value" :class="{'input-value-border': border}">
<text v-if="error!=null" class="error-text">{{error!.errMsg}}</text>
<scroll-view v-if="selectedPaths.length" class="selected-path" scroll-x="true">
<view class="selected-list">
<template v-for="(item, index) in selectedPaths">
<text class="text-color">{{item[mappingTextName]}}</text>
<text v-if="index<selectedPaths.length-1" class="input-split-line">{{split}}</text>
</template>
</view>
</scroll-view>
<text v-else-if="error==null&&!loading" class="placeholder">{{placeholder}}</text>
<view v-if="!readonly" class="arrow-area">
<view class="input-arrow"></view>
</view>
</view>
</slot>
<view v-if="loading && !isOpened" class="selected-loading">
<slot name="picker-loading" :loading="loading"></slot>
</view>
</view>
<view class="uni-data-tree-cover" v-if="isOpened" @click="handleClose"></view>
<view class="uni-data-tree-dialog" v-if="isOpened">
<view class="uni-popper__arrow"></view>
<view class="dialog-caption">
<view class="dialog-title-view">
<text class="dialog-title">{{popupTitle}}</text>
</view>
<view class="dialog-close" @click="handleClose">
<view class="dialog-close-plus" data-id="close"></view>
<view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
</view>
</view>
<view ref="pickerView" class="uni-data-pickerview">
<view v-if="error!=null" class="error">
<text class="error-text">{{error!.errMsg}}</text>
</view>
<scroll-view v-if="!isCloudDataList" :scroll-x="true">
<view class="selected-node-list">
<template v-for="(item, index) in selectedNodes">
<text class="selected-node-item" :class="{'selected-node-item-active':index==selectedIndex}"
@click="onTabSelect(index)">
{{item[mappingTextName]}}
</text>
</template>
</view>
</scroll-view>
<list-view class="list-view" :scroll-y="true">
<list-item class="list-item" v-for="(item, _) in currentDataList" @click="onNodeClick(item)">
<text class="item-text" :class="{'item-text-disabled': item['disable']}">{{item[mappingTextName]}}</text>
<text class="check" v-if="item[mappingValueName] == selectedNodes[selectedIndex][mappingValueName]"></text>
</list-item>
</list-view>
<view class="loading-cover" v-if="loading">
<slot name="pickerview-loading" :loading="loading"></slot>
</view>
</view>
</view>
</view>
</template>
<script>
import { dataPicker } from "../uni-data-pickerview/uni-data-picker.uts"
/**
* DataPicker 级联选择
* @description 支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。
* @tutorial https://ext.dcloud.net.cn/plugin?id=3796
* @property {String} popup-title 弹出窗口标题
* @property {Array} localdata 本地数据,参考
* @property {Boolean} border = [true|false] 是否有边框
* @property {Boolean} readonly = [true|false] 是否仅读
* @property {Boolean} preload = [true|false] 是否预加载数据
* @value true 开启预加载数据,点击弹出窗口后显示已加载数据
* @value false 关闭预加载数据,点击弹出窗口后开始加载数据
* @property {Boolean} step-searh = [true|false] 是否分布查询
* @value true 启用分布查询,仅查询当前选中节点
* @value false 关闭分布查询,一次查询出所有数据
* @property {String|DBFieldString} self-field 分布查询当前字段名称
* @property {String|DBFieldString} parent-field 分布查询父字段名称
* @property {String|DBCollectionString} collection 表名
* @property {String|DBFieldString} field 查询字段,多个字段用 `,` 分割
* @property {String} orderby 排序字段及正序倒叙设置
* @property {String|JQLString} where 查询条件
* @event {Function} popupshow 弹出的选择窗口打开时触发此事件
* @event {Function} popuphide 弹出的选择窗口关闭时触发此事件
*/
export default {
name: 'UniDataPicker',
emits: ['popupopened', 'popupclosed', 'nodeclick', 'change', 'input', 'update:modelValue', 'inputclick'],
mixins: [dataPicker],
props: {
popupTitle: {
type: String,
default: '请选择'
},
placeholder: {
type: String,
default: '请选择'
},
heightMobile: {
type: String,
default: ''
},
readonly: {
type: Boolean,
default: false
},
clearIcon: {
type: Boolean,
default: true
},
border: {
type: Boolean,
default: true
},
split: {
type: String,
default: '/'
},
ellipsis: {
type: Boolean,
default: true
}
},
data() {
return {
isOpened: false
}
},
computed: {
isShowClearIcon() : boolean {
if (this.readonly) {
return false
}
if (this.clearIcon && this.selectedPaths.length > 0) {
return true
}
return false
}
},
created() {
this.load()
},
methods: {
clear() {
},
load() {
if (this.isLocalData) {
this.loadLocalData()
} else if (this.isCloudDataList || this.isCloudDataTree) {
this.loadCloudDataPath()
}
},
show() {
this.isOpened = true
this.$emit('popupopened')
if (!this.hasCloudTreeData) {
this.loadData()
}
},
hide() {
this.isOpened = false
this.$emit('popupclosed')
},
handleInput() {
if (this.readonly) {
this.$emit('inputclick')
} else {
this.show()
}
},
handleClose() {
this.hide()
},
onFinish() {
this.selectedPaths = this.getChangeNodes()
this.$emit('change', this.selectedPaths)
this.hide()
}
}
}
</script>
<style>
@import url("../uni-data-pickerview/uni-data-pickerview.css");
.uni-data-tree {
position: relative;
}
.uni-data-tree-input {
position: relative;
}
.selected-loading {
display: flex;
justify-content: center;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.error-text {
flex: 1;
font-size: 12px;
color: #DD524D;
}
.input-value {
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
padding: 5px 5px;
padding-right: 5px;
overflow: hidden;
min-height: 28px;
}
.input-value-border {
border: 1px solid #e5e5e5;
border-radius: 5px;
}
.selected-path {
flex: 1;
flex-direction: row;
overflow: hidden;
}
.load-more {
width: 40px;
}
.selected-list {
flex-direction: row;
flex-wrap: nowrap;
}
.selected-item {
flex-direction: row;
flex-wrap: nowrap;
}
.text-color {
font-size: 14px;
color: #333;
}
.placeholder {
color: grey;
font-size: 14px;
}
.input-split-line {
opacity: .5;
margin-left: 1px;
margin-right: 1px;
}
.arrow-area {
position: relative;
padding: 0 12px;
margin-left: auto;
justify-content: center;
transform: rotate(-45deg);
transform-origin: center;
}
.input-arrow {
width: 8px;
height: 8px;
border-left: 2px solid #999;
border-bottom: 2px solid #999;
}
.uni-data-tree-cover {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, .4);
flex-direction: column;
z-index: 100;
}
.uni-data-tree-dialog {
position: fixed;
left: 0;
top: 20%;
right: 0;
bottom: 0;
background-color: #FFFFFF;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
flex-direction: column;
z-index: 102;
overflow: hidden;
}
.dialog-caption {
position: relative;
flex-direction: row;
}
.dialog-title-view {
flex: 1;
}
.dialog-title {
align-self: center;
padding: 0 10px;
line-height: 44px;
}
.dialog-close {
position: absolute;
top: 0;
right: 0;
bottom: 0;
flex-direction: row;
align-items: center;
padding: 0 15px;
}
.dialog-close-plus {
width: 16px;
height: 2px;
background-color: #666;
border-radius: 2px;
transform: rotate(45deg);
}
.dialog-close-rotate {
position: absolute;
transform: rotate(-45deg);
}
.uni-data-pickerview {
flex: 1;
}
.icon-clear {
display: flex;
align-items: center;
}
/* #ifdef H5 */
@media all and (min-width: 768px) {
.uni-data-tree-cover {
background-color: transparent;
}
.uni-data-tree-dialog {
position: absolute;
top: 55px;
height: auto;
min-height: 400px;
max-height: 50vh;
background-color: #fff;
border: 1px solid #EBEEF5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
overflow: unset;
}
.dialog-caption {
display: none;
}
}
/* #endif */
</style>

View File

@@ -0,0 +1,560 @@
<template>
<view class="uni-data-tree">
<view class="uni-data-tree-input" @click="handleInput">
<slot :options="options" :data="inputSelected" :error="errorMessage">
<view class="input-value" :class="{'input-value-border': border}">
<text v-if="errorMessage" class="selected-area error-text">{{errorMessage}}</text>
<view v-else-if="loading && !isOpened" class="selected-area">
<uni-load-more class="load-more" :contentText="loadMore" status="loading"></uni-load-more>
</view>
<scroll-view v-else-if="inputSelected.length" class="selected-area" scroll-x="true">
<view class="selected-list">
<view class="selected-item" v-for="(item,index) in inputSelected" :key="index">
<text class="text-color">{{item.text}}</text><text v-if="index<inputSelected.length-1"
class="input-split-line">{{split}}</text>
</view>
</view>
</scroll-view>
<text v-else class="selected-area placeholder">{{placeholder}}</text>
<view v-if="clearIcon && !readonly && inputSelected.length" class="icon-clear" @click.stop="clear">
<uni-icons type="clear" color="#c0c4cc" size="24"></uni-icons>
</view>
<view class="arrow-area" v-if="(!clearIcon || !inputSelected.length) && !readonly ">
<view class="input-arrow"></view>
</view>
</view>
</slot>
</view>
<view class="uni-data-tree-cover" v-if="isOpened" @click="handleClose"></view>
<view class="uni-data-tree-dialog" v-if="isOpened">
<view class="uni-popper__arrow"></view>
<view class="dialog-caption">
<view class="title-area">
<text class="dialog-title">{{popupTitle}}</text>
</view>
<view class="dialog-close" @click="handleClose">
<view class="dialog-close-plus" data-id="close"></view>
<view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
</view>
</view>
<data-picker-view class="picker-view" ref="pickerView" v-model="dataValue" :localdata="localdata"
:preload="preload" :collection="collection" :field="field" :orderby="orderby" :where="where"
:step-searh="stepSearh" :self-field="selfField" :parent-field="parentField" :managed-mode="true" :map="map"
:ellipsis="ellipsis" @change="onchange" @datachange="ondatachange" @nodeclick="onnodeclick">
</data-picker-view>
</view>
</view>
</template>
<script>
import dataPicker from "../uni-data-pickerview/uni-data-picker.js"
import DataPickerView from "../uni-data-pickerview/uni-data-pickerview.vue"
/**
* DataPicker 级联选择
* @description 支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。
* @tutorial https://ext.dcloud.net.cn/plugin?id=3796
* @property {String} popup-title 弹出窗口标题
* @property {Array} localdata 本地数据,参考
* @property {Boolean} border = [true|false] 是否有边框
* @property {Boolean} readonly = [true|false] 是否仅读
* @property {Boolean} preload = [true|false] 是否预加载数据
* @value true 开启预加载数据,点击弹出窗口后显示已加载数据
* @value false 关闭预加载数据,点击弹出窗口后开始加载数据
* @property {Boolean} step-searh = [true|false] 是否分布查询
* @value true 启用分布查询,仅查询当前选中节点
* @value false 关闭分布查询,一次查询出所有数据
* @property {String|DBFieldString} self-field 分布查询当前字段名称
* @property {String|DBFieldString} parent-field 分布查询父字段名称
* @property {String|DBCollectionString} collection 表名
* @property {String|DBFieldString} field 查询字段,多个字段用 `,` 分割
* @property {String} orderby 排序字段及正序倒叙设置
* @property {String|JQLString} where 查询条件
* @event {Function} popupshow 弹出的选择窗口打开时触发此事件
* @event {Function} popuphide 弹出的选择窗口关闭时触发此事件
*/
export default {
name: 'UniDataPicker',
emits: ['popupopened', 'popupclosed', 'nodeclick', 'input', 'change', 'update:modelValue','inputclick'],
mixins: [dataPicker],
components: {
DataPickerView
},
props: {
options: {
type: [Object, Array],
default () {
return {}
}
},
popupTitle: {
type: String,
default: '请选择'
},
placeholder: {
type: String,
default: '请选择'
},
heightMobile: {
type: String,
default: ''
},
readonly: {
type: Boolean,
default: false
},
clearIcon: {
type: Boolean,
default: true
},
border: {
type: Boolean,
default: true
},
split: {
type: String,
default: '/'
},
ellipsis: {
type: Boolean,
default: true
}
},
data() {
return {
isOpened: false,
inputSelected: []
}
},
created() {
this.$nextTick(() => {
this.load();
})
},
watch: {
localdata: {
handler() {
this.load()
},
deep: true
},
},
methods: {
clear() {
this._dispatchEvent([]);
},
onPropsChange() {
this._treeData = [];
this.selectedIndex = 0;
this.load();
},
load() {
if (this.readonly) {
this._processReadonly(this.localdata, this.dataValue);
return;
}
// 回显本地数据
if (this.isLocalData) {
this.loadData();
this.inputSelected = this.selected.slice(0);
} else if (this.isCloudDataList || this.isCloudDataTree) { // 回显 Cloud 数据
this.loading = true;
this.getCloudDataValue().then((res) => {
this.loading = false;
this.inputSelected = res;
}).catch((err) => {
this.loading = false;
this.errorMessage = err;
})
}
},
show() {
this.isOpened = true
setTimeout(() => {
this.$refs.pickerView.updateData({
treeData: this._treeData,
selected: this.selected,
selectedIndex: this.selectedIndex
})
}, 200)
this.$emit('popupopened')
},
hide() {
this.isOpened = false
this.$emit('popupclosed')
},
handleInput() {
if (this.readonly) {
this.$emit('inputclick')
return
}
this.show()
},
handleClose(e) {
this.hide()
},
onnodeclick(e) {
this.$emit('nodeclick', e)
},
ondatachange(e) {
this._treeData = this.$refs.pickerView._treeData
},
onchange(e) {
this.hide()
this.$nextTick(() => {
this.inputSelected = e;
})
this._dispatchEvent(e)
},
_processReadonly(dataList, value) {
var isTree = dataList.findIndex((item) => {
return item.children
})
if (isTree > -1) {
let inputValue
if (Array.isArray(value)) {
inputValue = value[value.length - 1]
if (typeof inputValue === 'object' && inputValue.value) {
inputValue = inputValue.value
}
} else {
inputValue = value
}
this.inputSelected = this._findNodePath(inputValue, this.localdata)
return
}
if (!this.hasValue) {
this.inputSelected = []
return
}
let result = []
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
var val = value[i]
var item = dataList.find((v) => {
return v.value == val
})
if (item) {
result.push(item)
}
}
} else {
let item = dataList.find((v) => {
return v.value == value;
});
if (item) {
result.push(item);
}
}
if (result.length) {
this.inputSelected = result
}
},
_filterForArray(data, valueArray) {
var result = []
for (let i = 0; i < valueArray.length; i++) {
var value = valueArray[i]
var found = data.find((item) => {
return item.value == value
})
if (found) {
result.push(found)
}
}
return result
},
_dispatchEvent(selected) {
let item = {}
if (selected.length) {
var value = new Array(selected.length)
for (var i = 0; i < selected.length; i++) {
value[i] = selected[i].value
}
item = selected[selected.length - 1]
} else {
item.value = ''
}
if (this.formItem) {
this.formItem.setValue(item.value)
}
this.$emit('input', item.value)
this.$emit('update:modelValue', item.value)
this.$emit('change', {
detail: {
value: selected
}
})
}
}
}
</script>
<style>
.uni-data-tree {
flex: 1;
position: relative;
font-size: 14px;
}
.error-text {
color: #DD524D;
}
.input-value {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
font-size: 14px;
/* line-height: 35px; */
padding: 0 10px;
padding-right: 5px;
overflow: hidden;
height: 35px;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
}
.input-value-border {
border: 1px solid #e5e5e5;
border-radius: 5px;
}
.selected-area {
flex: 1;
overflow: hidden;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.load-more {
/* #ifndef APP-NVUE */
margin-right: auto;
/* #endif */
/* #ifdef APP-NVUE */
width: 40px;
/* #endif */
}
.selected-list {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex-wrap: nowrap;
/* padding: 0 5px; */
}
.selected-item {
flex-direction: row;
/* padding: 0 1px; */
/* #ifndef APP-NVUE */
white-space: nowrap;
/* #endif */
}
.text-color {
color: #333;
}
.placeholder {
color: grey;
font-size: 12px;
}
.input-split-line {
opacity: .5;
}
.arrow-area {
position: relative;
width: 20px;
/* #ifndef APP-NVUE */
margin-bottom: 5px;
margin-left: auto;
display: flex;
/* #endif */
justify-content: center;
transform: rotate(-45deg);
transform-origin: center;
}
.input-arrow {
width: 7px;
height: 7px;
border-left: 1px solid #999;
border-bottom: 1px solid #999;
}
.uni-data-tree-cover {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, .4);
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
z-index: 100;
}
.uni-data-tree-dialog {
position: fixed;
left: 0;
/* #ifndef APP-NVUE */
top: 20%;
/* #endif */
/* #ifdef APP-NVUE */
top: 200px;
/* #endif */
right: 0;
bottom: 0;
background-color: #FFFFFF;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
z-index: 102;
overflow: hidden;
/* #ifdef APP-NVUE */
width: 750rpx;
/* #endif */
}
.dialog-caption {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
/* border-bottom: 1px solid #f0f0f0; */
}
.title-area {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
/* #ifndef APP-NVUE */
margin: auto;
/* #endif */
padding: 0 10px;
}
.dialog-title {
/* font-weight: bold; */
line-height: 44px;
}
.dialog-close {
position: absolute;
top: 0;
right: 0;
bottom: 0;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
padding: 0 15px;
}
.dialog-close-plus {
width: 16px;
height: 2px;
background-color: #666;
border-radius: 2px;
transform: rotate(45deg);
}
.dialog-close-rotate {
position: absolute;
transform: rotate(-45deg);
}
.picker-view {
flex: 1;
overflow: hidden;
}
.icon-clear {
display: flex;
align-items: center;
}
/* #ifdef H5 */
@media all and (min-width: 768px) {
.uni-data-tree-cover {
background-color: transparent;
}
.uni-data-tree-dialog {
position: absolute;
top: 55px;
height: auto;
min-height: 400px;
max-height: 50vh;
background-color: #fff;
border: 1px solid #EBEEF5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
overflow: unset;
}
.dialog-caption {
display: none;
}
.icon-clear {
/* margin-right: 5px; */
}
}
/* #endif */
/* picker 弹出层通用的指示小三角, todo:扩展至上下左右方向定位 */
/* #ifndef APP-NVUE */
.uni-popper__arrow,
.uni-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
}
.uni-popper__arrow {
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
top: -6px;
left: 10%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #EBEEF5;
}
.uni-popper__arrow::after {
content: " ";
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
/* #endif */
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,622 @@
export default {
props: {
localdata: {
type: [Array, Object],
default () {
return []
}
},
spaceInfo: {
type: Object,
default () {
return {}
}
},
collection: {
type: String,
default: ''
},
action: {
type: String,
default: ''
},
field: {
type: String,
default: ''
},
orderby: {
type: String,
default: ''
},
where: {
type: [String, Object],
default: ''
},
pageData: {
type: String,
default: 'add'
},
pageCurrent: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 500
},
getcount: {
type: [Boolean, String],
default: false
},
getone: {
type: [Boolean, String],
default: false
},
gettree: {
type: [Boolean, String],
default: false
},
manual: {
type: Boolean,
default: false
},
value: {
type: [Array, String, Number],
default () {
return []
}
},
modelValue: {
type: [Array, String, Number],
default () {
return []
}
},
preload: {
type: Boolean,
default: false
},
stepSearh: {
type: Boolean,
default: true
},
selfField: {
type: String,
default: ''
},
parentField: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: false
},
map: {
type: Object,
default () {
return {
text: "text",
value: "value"
}
}
}
},
data() {
return {
loading: false,
errorMessage: '',
loadMore: {
contentdown: '',
contentrefresh: '',
contentnomore: ''
},
dataList: [],
selected: [],
selectedIndex: 0,
page: {
current: this.pageCurrent,
size: this.pageSize,
count: 0
}
}
},
computed: {
isLocalData() {
return !this.collection.length;
},
isCloudData() {
return this.collection.length > 0;
},
isCloudDataList() {
return (this.isCloudData && (!this.parentField && !this.selfField));
},
isCloudDataTree() {
return (this.isCloudData && this.parentField && this.selfField);
},
dataValue() {
let isModelValue = Array.isArray(this.modelValue) ? (this.modelValue.length > 0) : (this.modelValue !== null ||
this.modelValue !== undefined);
return isModelValue ? this.modelValue : this.value;
},
hasValue() {
if (typeof this.dataValue === 'number') {
return true
}
return (this.dataValue != null) && (this.dataValue.length > 0)
}
},
created() {
this.$watch(() => {
var al = [];
['pageCurrent',
'pageSize',
'spaceInfo',
'value',
'modelValue',
'localdata',
'collection',
'action',
'field',
'orderby',
'where',
'getont',
'getcount',
'gettree'
].forEach(key => {
al.push(this[key])
});
return al
}, (newValue, oldValue) => {
let needReset = false
for (let i = 2; i < newValue.length; i++) {
if (newValue[i] != oldValue[i]) {
needReset = true
break
}
}
if (newValue[0] != oldValue[0]) {
this.page.current = this.pageCurrent
}
this.page.size = this.pageSize
this.onPropsChange()
})
this._treeData = []
},
methods: {
onPropsChange() {
this._treeData = [];
},
// 填充 pickview 数据
async loadData() {
if (this.isLocalData) {
this.loadLocalData();
} else if (this.isCloudDataList) {
this.loadCloudDataList();
} else if (this.isCloudDataTree) {
this.loadCloudDataTree();
}
},
// 加载本地数据
async loadLocalData() {
this._treeData = [];
this._extractTree(this.localdata, this._treeData);
let inputValue = this.dataValue;
if (inputValue === undefined) {
return;
}
if (Array.isArray(inputValue)) {
inputValue = inputValue[inputValue.length - 1];
if (typeof inputValue === 'object' && inputValue[this.map.value]) {
inputValue = inputValue[this.map.value];
}
}
this.selected = this._findNodePath(inputValue, this.localdata);
},
// 加载 Cloud 数据 (单列)
async loadCloudDataList() {
if (this.loading) {
return;
}
this.loading = true;
try {
let response = await this.getCommand();
let responseData = response.result.data;
this._treeData = responseData;
this._updateBindData();
this._updateSelected();
this.onDataChange();
} catch (e) {
this.errorMessage = e;
} finally {
this.loading = false;
}
},
// 加载 Cloud 数据 (树形)
async loadCloudDataTree() {
if (this.loading) {
return;
}
this.loading = true;
try {
let commandOptions = {
field: this._cloudDataPostField(),
where: this._cloudDataTreeWhere()
};
if (this.gettree) {
commandOptions.startwith = `${this.selfField}=='${this.dataValue}'`;
}
let response = await this.getCommand(commandOptions);
let responseData = response.result.data;
this._treeData = responseData;
this._updateBindData();
this._updateSelected();
this.onDataChange();
} catch (e) {
this.errorMessage = e;
} finally {
this.loading = false;
}
},
// 加载 Cloud 数据 (节点)
async loadCloudDataNode(callback) {
if (this.loading) {
return;
}
this.loading = true;
try {
let commandOptions = {
field: this._cloudDataPostField(),
where: this._cloudDataNodeWhere()
};
let response = await this.getCommand(commandOptions);
let responseData = response.result.data;
callback(responseData);
} catch (e) {
this.errorMessage = e;
} finally {
this.loading = false;
}
},
// 回显 Cloud 数据
getCloudDataValue() {
if (this.isCloudDataList) {
return this.getCloudDataListValue();
}
if (this.isCloudDataTree) {
return this.getCloudDataTreeValue();
}
},
// 回显 Cloud 数据 (单列)
getCloudDataListValue() {
// 根据 field's as value标识匹配 where 条件
let where = [];
let whereField = this._getForeignKeyByField();
if (whereField) {
where.push(`${whereField} == '${this.dataValue}'`)
}
where = where.join(' || ');
if (this.where) {
where = `(${this.where}) && (${where})`
}
return this.getCommand({
field: this._cloudDataPostField(),
where
}).then((res) => {
this.selected = res.result.data;
return res.result.data;
});
},
// 回显 Cloud 数据 (树形)
getCloudDataTreeValue() {
return this.getCommand({
field: this._cloudDataPostField(),
getTreePath: {
startWith: `${this.selfField}=='${this.dataValue}'`
}
}).then((res) => {
let treePath = [];
this._extractTreePath(res.result.data, treePath);
this.selected = treePath;
return treePath;
});
},
getCommand(options = {}) {
/* eslint-disable no-undef */
let db = uniCloud.database(this.spaceInfo)
const action = options.action || this.action
if (action) {
db = db.action(action)
}
const collection = options.collection || this.collection
db = db.collection(collection)
const where = options.where || this.where
if (!(!where || !Object.keys(where).length)) {
db = db.where(where)
}
const field = options.field || this.field
if (field) {
db = db.field(field)
}
const orderby = options.orderby || this.orderby
if (orderby) {
db = db.orderBy(orderby)
}
const current = options.pageCurrent !== undefined ? options.pageCurrent : this.page.current
const size = options.pageSize !== undefined ? options.pageSize : this.page.size
const getCount = options.getcount !== undefined ? options.getcount : this.getcount
const getTree = options.gettree !== undefined ? options.gettree : this.gettree
const getOptions = {
getCount,
getTree
}
if (options.getTreePath) {
getOptions.getTreePath = options.getTreePath
}
db = db.skip(size * (current - 1)).limit(size).get(getOptions)
return db
},
_cloudDataPostField() {
let fields = [this.field];
if (this.parentField) {
fields.push(`${this.parentField} as parent_value`);
}
return fields.join(',');
},
_cloudDataTreeWhere() {
let result = []
let selected = this.selected
let parentField = this.parentField
if (parentField) {
result.push(`${parentField} == null || ${parentField} == ""`)
}
if (selected.length) {
for (var i = 0; i < selected.length - 1; i++) {
result.push(`${parentField} == '${selected[i].value}'`)
}
}
let where = []
if (this.where) {
where.push(`(${this.where})`)
}
if (result.length) {
where.push(`(${result.join(' || ')})`)
}
return where.join(' && ')
},
_cloudDataNodeWhere() {
let where = []
let selected = this.selected;
if (selected.length) {
where.push(`${this.parentField} == '${selected[selected.length - 1].value}'`);
}
where = where.join(' || ');
if (this.where) {
return `(${this.where}) && (${where})`
}
return where
},
_getWhereByForeignKey() {
let result = []
let whereField = this._getForeignKeyByField();
if (whereField) {
result.push(`${whereField} == '${this.dataValue}'`)
}
if (this.where) {
return `(${this.where}) && (${result.join(' || ')})`
}
return result.join(' || ')
},
_getForeignKeyByField() {
let fields = this.field.split(',');
let whereField = null;
for (let i = 0; i < fields.length; i++) {
const items = fields[i].split('as');
if (items.length < 2) {
continue;
}
if (items[1].trim() === 'value') {
whereField = items[0].trim();
break;
}
}
return whereField;
},
_updateBindData(node) {
const {
dataList,
hasNodes
} = this._filterData(this._treeData, this.selected)
let isleaf = this._stepSearh === false && !hasNodes
if (node) {
node.isleaf = isleaf
}
this.dataList = dataList
this.selectedIndex = dataList.length - 1
if (!isleaf && this.selected.length < dataList.length) {
this.selected.push({
value: null,
text: "请选择"
})
}
return {
isleaf,
hasNodes
}
},
_updateSelected() {
let dl = this.dataList
let sl = this.selected
let textField = this.map.text
let valueField = this.map.value
for (let i = 0; i < sl.length; i++) {
let value = sl[i].value
let dl2 = dl[i]
for (let j = 0; j < dl2.length; j++) {
let item2 = dl2[j]
if (item2[valueField] === value) {
sl[i].text = item2[textField]
break
}
}
}
},
_filterData(data, paths) {
let dataList = []
let hasNodes = true
dataList.push(data.filter((item) => {
return (item.parent_value === null || item.parent_value === undefined || item.parent_value === '')
}))
for (let i = 0; i < paths.length; i++) {
let value = paths[i].value
let nodes = data.filter((item) => {
return item.parent_value === value
})
if (nodes.length) {
dataList.push(nodes)
} else {
hasNodes = false
}
}
return {
dataList,
hasNodes
}
},
_extractTree(nodes, result, parent_value) {
let list = result || []
let valueField = this.map.value
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
let child = {}
for (let key in node) {
if (key !== 'children') {
child[key] = node[key]
}
}
if (parent_value !== null && parent_value !== undefined && parent_value !== '') {
child.parent_value = parent_value
}
result.push(child)
let children = node.children
if (children) {
this._extractTree(children, result, node[valueField])
}
}
},
_extractTreePath(nodes, result) {
let list = result || []
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
let child = {}
for (let key in node) {
if (key !== 'children') {
child[key] = node[key]
}
}
result.push(child)
let children = node.children
if (children) {
this._extractTreePath(children, result)
}
}
},
_findNodePath(key, nodes, path = []) {
let textField = this.map.text
let valueField = this.map.value
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
let children = node.children
let text = node[textField]
let value = node[valueField]
path.push({
value,
text
})
if (value === key) {
return path
}
if (children) {
const p = this._findNodePath(key, children, path)
if (p.length) {
return p
}
}
path.pop()
}
return []
}
}
}

View File

@@ -0,0 +1,693 @@
export type PaginationType = {
current : number,
size : number,
count : number
}
export type LoadMoreType = {
contentdown : string,
contentrefresh : string,
contentnomore : string
}
export type SelectedItemType = {
name : string,
value : string,
}
export type GetCommandOptions = {
collection ?: UTSJSONObject,
field ?: string,
orderby ?: string,
where ?: any,
pageData ?: string,
pageCurrent ?: number,
pageSize ?: number,
getCount ?: boolean,
getTree ?: any,
getTreePath ?: UTSJSONObject,
startwith ?: string,
limitlevel ?: number,
groupby ?: string,
groupField ?: string,
distinct ?: boolean,
pageIndistinct ?: boolean,
foreignKey ?: string,
loadtime ?: string,
manual ?: boolean
}
const DefaultSelectedNode = {
text: '请选择',
value: ''
}
export const dataPicker = defineMixin({
props: {
localdata: {
type: Array as PropType<Array<UTSJSONObject>>,
default: [] as Array<UTSJSONObject>
},
collection: {
type: Object,
default: ''
},
field: {
type: String,
default: ''
},
orderby: {
type: String,
default: ''
},
where: {
type: Object,
default: ''
},
pageData: {
type: String,
default: 'add'
},
pageCurrent: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 20
},
getcount: {
type: Boolean,
default: false
},
gettree: {
type: Object,
default: ''
},
gettreepath: {
type: Object,
default: ''
},
startwith: {
type: String,
default: ''
},
limitlevel: {
type: Number,
default: 10
},
groupby: {
type: String,
default: ''
},
groupField: {
type: String,
default: ''
},
distinct: {
type: Boolean,
default: false
},
pageIndistinct: {
type: Boolean,
default: false
},
foreignKey: {
type: String,
default: ''
},
loadtime: {
type: String,
default: 'auto'
},
manual: {
type: Boolean,
default: false
},
preload: {
type: Boolean,
default: false
},
stepSearh: {
type: Boolean,
default: true
},
selfField: {
type: String,
default: ''
},
parentField: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: false
},
value: {
type: Object,
default: ''
},
modelValue: {
type: Object,
default: ''
},
defaultProps: {
type: Object as PropType<UTSJSONObject>,
}
},
data() {
return {
loading: false,
error: null as UniCloudError | null,
treeData: [] as Array<UTSJSONObject>,
selectedIndex: 0,
selectedNodes: [] as Array<UTSJSONObject>,
selectedPages: [] as Array<UTSJSONObject>[],
selectedValue: '',
selectedPaths: [] as Array<UTSJSONObject>,
pagination: {
current: 1,
size: 20,
count: 0
} as PaginationType
}
},
computed: {
mappingTextName() : string {
// TODO
return (this.defaultProps != null) ? this.defaultProps!.getString('text', 'text') : 'text'
},
mappingValueName() : string {
// TODO
return (this.defaultProps != null) ? this.defaultProps!.getString('value', 'value') : 'value'
},
currentDataList() : Array<UTSJSONObject> {
if (this.selectedIndex > this.selectedPages.length - 1) {
return [] as Array<UTSJSONObject>
}
return this.selectedPages[this.selectedIndex]
},
isLocalData() : boolean {
return this.localdata.length > 0
},
isCloudData() : boolean {
return this._checkIsNotNull(this.collection)
},
isCloudDataList() : boolean {
return (this.isCloudData && (this.parentField.length == 0 && this.selfField.length == 0))
},
isCloudDataTree() : boolean {
return (this.isCloudData && this.parentField.length > 0 && this.selfField.length > 0)
},
dataValue() : any {
return this.hasModelValue ? this.modelValue : this.value
},
hasCloudTreeData() : boolean {
return this.treeData.length > 0
},
hasModelValue() : boolean {
if (typeof this.modelValue == 'string') {
const valueString = this.modelValue as string
return (valueString.length > 0)
} else if (Array.isArray(this.modelValue)) {
const valueArray = this.modelValue as Array<string>
return (valueArray.length > 0)
}
return false
},
hasCloudDataValue() : boolean {
if (typeof this.dataValue == 'string') {
const valueString = this.dataValue as string
return (valueString.length > 0)
}
return false
}
},
created() {
this.pagination.current = this.pageCurrent
this.pagination.size = this.pageSize
this.$watch(
() : any => [
this.pageCurrent,
this.pageSize,
this.localdata,
this.value,
this.collection,
this.field,
this.getcount,
this.orderby,
this.where,
this.groupby,
this.groupField,
this.distinct
],
(newValue : Array<any>, oldValue : Array<any>) => {
this.pagination.size = this.pageSize
if (newValue[0] !== oldValue[0]) {
this.pagination.current = this.pageCurrent
}
this.onPropsChange()
}
)
},
methods: {
onPropsChange() {
this.selectedIndex = 0
this.treeData.length = 0
this.selectedNodes.length = 0
this.selectedPages.length = 0
this.selectedPaths.length = 0
// 加载数据
this.$nextTick(() => {
this.loadData()
})
},
onTabSelect(index : number) {
this.selectedIndex = index
},
onNodeClick(nodeData : UTSJSONObject) {
if (nodeData.getBoolean('disable', false)) {
return
}
const isLeaf = this._checkIsLeafNode(nodeData)
this._trimSelectedNodes(nodeData)
this.$emit('nodeclick', nodeData)
if (this.isLocalData) {
if (isLeaf || !this._checkHasChildren(nodeData)) {
this.onFinish()
}
} else if (this.isCloudDataList) {
this.onFinish()
} else if (this.isCloudDataTree) {
if (isLeaf) {
this.onFinish()
} else if (!this._checkHasChildren(nodeData)) {
// 尝试请求一次,如果没有返回数据标记为叶子节点
this.loadCloudDataNode(nodeData)
}
}
},
getChangeNodes(): Array<UTSJSONObject> {
const nodes: Array<UTSJSONObject> = []
this.selectedNodes.forEach((node : UTSJSONObject) => {
const newNode: UTSJSONObject = {}
newNode[this.mappingTextName] = node.getString(this.mappingTextName)
newNode[this.mappingValueName] = node.getString(this.mappingValueName)
nodes.push(newNode)
})
return nodes
},
onFinish() { },
// 加载数据(自动判定环境)
loadData() {
if (this.isLocalData) {
this.loadLocalData()
} else if (this.isCloudDataList) {
this.loadCloudDataList()
} else if (this.isCloudDataTree) {
this.loadCloudDataTree()
}
},
// 加载本地数据
loadLocalData() {
this.treeData = this.localdata
if (Array.isArray(this.dataValue)) {
const value = this.dataValue as Array<UTSJSONObject>
this.selectedPaths = value.slice(0)
this._pushSelectedTreeNodes(value, this.localdata)
} else {
this._pushSelectedNodes(this.localdata)
}
},
// 加载 Cloud 数据 (单列)
loadCloudDataList() {
this._loadCloudData(null, (data : Array<UTSJSONObject>) => {
this.treeData = data
this._pushSelectedNodes(data)
})
},
// 加载 Cloud 数据 (树形)
loadCloudDataTree() {
let commandOptions = {
field: this._cloudDataPostField(),
where: this._cloudDataTreeWhere(),
getTree: true
} as GetCommandOptions
if (this._checkIsNotNull(this.gettree)) {
commandOptions.startwith = `${this.selfField}=='${this.dataValue as string}'`
}
this._loadCloudData(commandOptions, (data : Array<UTSJSONObject>) => {
this.treeData = data
if (this.selectedPaths.length > 0) {
this._pushSelectedTreeNodes(this.selectedPaths, data)
} else {
this._pushSelectedNodes(data)
}
})
},
// 加载 Cloud 数据 (节点)
loadCloudDataNode(nodeData : UTSJSONObject) {
const commandOptions = {
field: this._cloudDataPostField(),
where: this._cloudDataNodeWhere()
} as GetCommandOptions
this._loadCloudData(commandOptions, (data : Array<UTSJSONObject>) => {
nodeData['children'] = data
if (data.length == 0) {
nodeData['isleaf'] = true
this.onFinish()
} else {
this._pushSelectedNodes(data)
}
})
},
// 回显 Cloud Tree Path
loadCloudDataPath() {
if (!this.hasCloudDataValue) {
return
}
const command : GetCommandOptions = {}
// 单列
if (this.isCloudDataList) {
// 根据 field's as value标识匹配 where 条件
let where : Array<string> = [];
let whereField = this._getForeignKeyByField();
if (whereField.length > 0) {
where.push(`${whereField} == '${this.dataValue as string}'`)
}
let whereString = where.join(' || ')
if (this._checkIsNotNull(this.where)) {
whereString = `(${this.where}) && (${whereString})`
}
command.field = this._cloudDataPostField()
command.where = whereString
}
// 树形
if (this.isCloudDataTree) {
command.field = this._cloudDataPostField()
command.getTreePath = {
startWith: `${this.selfField}=='${this.dataValue as string}'`
}
}
this._loadCloudData(command, (data : Array<UTSJSONObject>) => {
this._extractTreePath(data, this.selectedPaths)
})
},
_loadCloudData(options ?: GetCommandOptions, callback ?: ((data : Array<UTSJSONObject>) => void)) {
if (this.loading) {
return
}
this.loading = true
this.error = null
this._getCommand(options).then((response : UniCloudDBGetResult) => {
callback?.(response.data)
}).catch((err : any | null) => {
this.error = err as UniCloudError
}).finally(() => {
this.loading = false
})
},
_cloudDataPostField() : string {
let fields = [this.field];
if (this.parentField.length > 0) {
fields.push(`${this.parentField} as parent_value`)
}
return fields.join(',')
},
_cloudDataTreeWhere() : string {
let result : Array<string> = []
let selectedNodes = this.selectedNodes.length > 0 ? this.selectedNodes : this.selectedPaths
let parentField = this.parentField
if (parentField.length > 0) {
result.push(`${parentField} == null || ${parentField} == ""`)
}
if (selectedNodes.length > 0) {
for (var i = 0; i < selectedNodes.length - 1; i++) {
const parentFieldValue = selectedNodes[i].getString('value', '')
result.push(`${parentField} == '${parentFieldValue}'`)
}
}
let where : Array<string> = []
if (this._checkIsNotNull(this.where)) {
where.push(`(${this.where as string})`)
}
if (result.length > 0) {
where.push(`(${result.join(' || ')})`)
}
return where.join(' && ')
},
_cloudDataNodeWhere() : string {
const where : Array<string> = []
if (this.selectedNodes.length > 0) {
const value = this.selectedNodes[this.selectedNodes.length - 1].getString('value', '')
where.push(`${this.parentField} == '${value}'`)
}
let whereString = where.join(' || ')
if (this._checkIsNotNull(this.where)) {
return `(${this.where as string}) && (${whereString})`
}
return whereString
},
_getWhereByForeignKey() : string {
let result : Array<string> = []
let whereField = this._getForeignKeyByField();
if (whereField.length > 0) {
result.push(`${whereField} == '${this.dataValue as string}'`)
}
if (this._checkIsNotNull(this.where)) {
return `(${this.where}) && (${result.join(' || ')})`
}
return result.join(' || ')
},
_getForeignKeyByField() : string {
const fields = this.field.split(',')
let whereField = ''
for (let i = 0; i < fields.length; i++) {
const items = fields[i].split('as')
if (items.length < 2) {
continue
}
if (items[1].trim() === 'value') {
whereField = items[0].trim()
break
}
}
return whereField
},
_getCommand(options ?: GetCommandOptions) : Promise<UniCloudDBGetResult> {
let db = uniCloud.databaseForJQL()
let collection = Array.isArray(this.collection) ? db.collection(...(this.collection as Array<any>)) : db.collection(this.collection)
let filter : UniCloudDBFilter | null = null
if (this.foreignKey.length > 0) {
filter = collection.foreignKey(this.foreignKey)
}
const where : any = options?.where ?? this.where
if (typeof where == 'string') {
const whereString = where as string
if (whereString.length > 0) {
filter = (filter != null) ? filter.where(where) : collection.where(where)
}
} else {
filter = (filter != null) ? filter.where(where) : collection.where(where)
}
let query : UniCloudDBQuery | null = null
if (this.field.length > 0) {
query = (filter != null) ? filter.field(this.field) : collection.field(this.field)
}
if (this.groupby.length > 0) {
if (query != null) {
query = query.groupBy(this.groupby)
} else if (filter != null) {
query = filter.groupBy(this.groupby)
}
}
if (this.groupField.length > 0) {
if (query != null) {
query = query.groupField(this.groupField)
} else if (filter != null) {
query = filter.groupField(this.groupField)
}
}
if (this.distinct == true) {
if (query != null) {
query = query.distinct(this.field)
} else if (filter != null) {
query = filter.distinct(this.field)
}
}
if (this.orderby.length > 0) {
if (query != null) {
query = query.orderBy(this.orderby)
} else if (filter != null) {
query = filter.orderBy(this.orderby)
}
}
const size = this.pagination.size
const current = this.pagination.current
if (query != null) {
query = query.skip(size * (current - 1)).limit(size)
} else if (filter != null) {
query = filter.skip(size * (current - 1)).limit(size)
} else {
query = collection.skip(size * (current - 1)).limit(size)
}
const getOptions = {}
const treeOptions = {
limitLevel: this.limitlevel,
startWith: this.startwith
}
if (this.getcount == true) {
getOptions['getCount'] = this.getcount
}
const getTree : any = options?.getTree ?? this.gettree
if (typeof getTree == 'string') {
const getTreeString = getTree as string
if (getTreeString.length > 0) {
getOptions['getTree'] = treeOptions
}
} else if (typeof getTree == 'object') {
getOptions['getTree'] = treeOptions
} else {
getOptions['getTree'] = getTree
}
const getTreePath = options?.getTreePath ?? this.gettreepath
if (typeof getTreePath == 'string') {
const getTreePathString = getTreePath as string
if (getTreePathString.length > 0) {
getOptions['getTreePath'] = getTreePath
}
} else {
getOptions['getTreePath'] = getTreePath
}
return query.get(getOptions)
},
_checkIsNotNull(value : any) : boolean {
if (typeof value == 'string') {
const valueString = value as string
return (valueString.length > 0)
} else if (value instanceof UTSJSONObject) {
return true
}
return false
},
_checkIsLeafNode(nodeData : UTSJSONObject) : boolean {
if (this.selectedIndex >= this.limitlevel) {
return true
}
if (nodeData.getBoolean('isleaf', false)) {
return true
}
return false
},
_checkHasChildren(nodeData : UTSJSONObject) : boolean {
const children = nodeData.getArray('children') ?? ([] as Array<any>)
return children.length > 0
},
_pushSelectedNodes(nodes : Array<UTSJSONObject>) {
this.selectedNodes.push(DefaultSelectedNode)
this.selectedPages.push(nodes)
this.selectedIndex = this.selectedPages.length - 1
},
_trimSelectedNodes(nodeData : UTSJSONObject) {
this.selectedNodes.splice(this.selectedIndex)
this.selectedNodes.push(nodeData)
if (this.selectedPages.length > 0) {
this.selectedPages.splice(this.selectedIndex + 1)
}
const children = nodeData.getArray<UTSJSONObject>('children') ?? ([] as Array<UTSJSONObject>)
if (children.length > 0) {
this.selectedNodes.push(DefaultSelectedNode)
this.selectedPages.push(children)
}
this.selectedIndex = this.selectedPages.length - 1
},
_pushSelectedTreeNodes(paths : Array<UTSJSONObject>, nodes : Array<UTSJSONObject>) {
let children : Array<UTSJSONObject> = nodes
paths.forEach((node : UTSJSONObject) => {
const findNode = children.find((item : UTSJSONObject) : boolean => {
return (item.getString(this.mappingValueName) == node.getString(this.mappingValueName))
})
if (findNode != null) {
this.selectedPages.push(children)
this.selectedNodes.push(node)
children = findNode.getArray<UTSJSONObject>('children') ?? ([] as Array<UTSJSONObject>)
}
})
this.selectedIndex = this.selectedPages.length - 1
},
_extractTreePath(nodes : Array<UTSJSONObject>, result : Array<UTSJSONObject>) {
if (nodes.length == 0) {
return
}
const node = nodes[0]
result.push(node)
const children = node.getArray<UTSJSONObject>('children')
if (Array.isArray(children) && children!.length > 0) {
this._extractTreePath(children, result)
}
}
}
})

View File

@@ -0,0 +1,76 @@
.uni-data-pickerview {
position: relative;
flex-direction: column;
overflow: hidden;
}
.loading-cover {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
align-items: center;
justify-content: center;
background-color: rgba(150, 150, 150, .1);
}
.error {
background-color: #fff;
padding: 15px;
}
.error-text {
color: #DD524D;
}
.selected-node-list {
flex-direction: row;
flex-wrap: nowrap;
}
.selected-node-item {
margin-left: 10px;
margin-right: 10px;
padding: 8px 10px 8px 10px;
border-bottom: 2px solid transparent;
}
.selected-node-item-active {
color: #007aff;
border-bottom-color: #007aff;
}
.list-view {
flex: 1;
}
.list-item {
flex-direction: row;
justify-content: space-between;
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
}
.item-text {
color: #333333;
}
.item-text-disabled {
opacity: .5;
}
.item-text-overflow {
overflow: hidden;
}
.check {
margin-right: 5px;
border: 2px solid #007aff;
border-left: 0;
border-top: 0;
height: 12px;
width: 6px;
transform-origin: center;
transform: rotate(45deg);
}

View File

@@ -0,0 +1,69 @@
<template>
<view class="uni-data-pickerview">
<view v-if="error!=null" class="error">
<text class="error-text">{{error!.errMsg}}</text>
</view>
<scroll-view v-if="!isCloudDataList" :scroll-x="true">
<view class="selected-node-list">
<template v-for="(item, index) in selectedNodes">
<text class="selected-node-item" :class="{'selected-node-item-active':index==selectedIndex}"
@click="onTabSelect(index)">
{{item[mappingTextName]}}
</text>
</template>
</view>
</scroll-view>
<list-view class="list-view" :scroll-y="true">
<list-item class="list-item" v-for="(item, _) in currentDataList" @click="onNodeClick(item)">
<text class="item-text" :class="{'item-text-disabled': item['disable']}">{{item[mappingTextName]}}</text>
<text class="check" v-if="item[mappingValueName] == selectedNodes[selectedIndex][mappingValueName]"></text>
</list-item>
</list-view>
<view class="loading-cover" v-if="loading">
<slot name="pickerview-loading" :loading="loading"></slot>
</view>
</view>
</template>
<script>
import { dataPicker } from "./uni-data-picker.uts"
/**
* DataPickerview
* @description uni-data-pickerview
* @tutorial https://ext.dcloud.net.cn/plugin?id=3796
* @property {Array} localdata 本地数据,参考
* @property {Boolean} step-searh = [true|false] 是否分布查询
* @value true 启用分布查询,仅查询当前选中节点
* @value false 关闭分布查询,一次查询出所有数据
* @property {String|DBFieldString} self-field 分布查询当前字段名称
* @property {String|DBFieldString} parent-field 分布查询父字段名称
* @property {String|DBCollectionString} collection 表名
* @property {String|DBFieldString} field 查询字段,多个字段用 `,` 分割
* @property {String} orderby 排序字段及正序倒叙设置
* @property {String|JQLString} where 查询条件
*/
export default {
name: 'UniDataPickerView',
emits: ['nodeclick', 'change', 'update:modelValue'],
mixins: [dataPicker],
props: {
ellipsis: {
type: Boolean,
default: true
}
},
created() {
this.loadData()
},
methods: {
onFinish() {
this.$emit('change', this.getChangeNodes())
}
}
}
</script>
<style>
@import url("uni-data-pickerview.css");
</style>

View File

@@ -0,0 +1,323 @@
<template>
<view class="uni-data-pickerview">
<scroll-view v-if="!isCloudDataList" class="selected-area" scroll-x="true">
<view class="selected-list">
<view
class="selected-item"
v-for="(item,index) in selected"
:key="index"
:class="{
'selected-item-active':index == selectedIndex
}"
@click="handleSelect(index)"
>
<text>{{item.text || ''}}</text>
</view>
</view>
</scroll-view>
<view class="tab-c">
<scroll-view class="list" :scroll-y="true">
<view class="item" :class="{'is-disabled': !!item.disable}" v-for="(item, j) in dataList[selectedIndex]" :key="j"
@click="handleNodeClick(item, selectedIndex, j)">
<text class="item-text">{{item[map.text]}}</text>
<view class="check" v-if="selected.length > selectedIndex && item[map.value] == selected[selectedIndex].value"></view>
</view>
</scroll-view>
<view class="loading-cover" v-if="loading">
<uni-load-more class="load-more" :contentText="loadMore" status="loading"></uni-load-more>
</view>
<view class="error-message" v-if="errorMessage">
<text class="error-text">{{errorMessage}}</text>
</view>
</view>
</view>
</template>
<script>
import dataPicker from "./uni-data-picker.js"
/**
* DataPickerview
* @description uni-data-pickerview
* @tutorial https://ext.dcloud.net.cn/plugin?id=3796
* @property {Array} localdata 本地数据,参考
* @property {Boolean} step-searh = [true|false] 是否分布查询
* @value true 启用分布查询,仅查询当前选中节点
* @value false 关闭分布查询,一次查询出所有数据
* @property {String|DBFieldString} self-field 分布查询当前字段名称
* @property {String|DBFieldString} parent-field 分布查询父字段名称
* @property {String|DBCollectionString} collection 表名
* @property {String|DBFieldString} field 查询字段,多个字段用 `,` 分割
* @property {String} orderby 排序字段及正序倒叙设置
* @property {String|JQLString} where 查询条件
*/
export default {
name: 'UniDataPickerView',
emits: ['nodeclick', 'change', 'datachange', 'update:modelValue'],
mixins: [dataPicker],
props: {
managedMode: {
type: Boolean,
default: false
},
ellipsis: {
type: Boolean,
default: true
}
},
created() {
if (!this.managedMode) {
this.$nextTick(() => {
this.loadData();
})
}
},
methods: {
onPropsChange() {
this._treeData = [];
this.selectedIndex = 0;
this.$nextTick(() => {
this.loadData();
})
},
handleSelect(index) {
this.selectedIndex = index;
},
handleNodeClick(item, i, j) {
if (item.disable) {
return;
}
const node = this.dataList[i][j];
const text = node[this.map.text];
const value = node[this.map.value];
if (i < this.selected.length - 1) {
this.selected.splice(i, this.selected.length - i)
this.selected.push({
text,
value
})
} else if (i === this.selected.length - 1) {
this.selected.splice(i, 1, {
text,
value
})
}
if (node.isleaf) {
this.onSelectedChange(node, node.isleaf)
return
}
const {
isleaf,
hasNodes
} = this._updateBindData()
// 本地数据
if (this.isLocalData) {
this.onSelectedChange(node, (!hasNodes || isleaf))
} else if (this.isCloudDataList) { // Cloud 数据 (单列)
this.onSelectedChange(node, true)
} else if (this.isCloudDataTree) { // Cloud 数据 (树形)
if (isleaf) {
this.onSelectedChange(node, node.isleaf)
} else if (!hasNodes) { // 请求一次服务器以确定是否为叶子节点
this.loadCloudDataNode((data) => {
if (!data.length) {
node.isleaf = true
} else {
this._treeData.push(...data)
this._updateBindData(node)
}
this.onSelectedChange(node, node.isleaf)
})
}
}
},
updateData(data) {
this._treeData = data.treeData
this.selected = data.selected
if (!this._treeData.length) {
this.loadData()
} else {
//this.selected = data.selected
this._updateBindData()
}
},
onDataChange() {
this.$emit('datachange');
},
onSelectedChange(node, isleaf) {
if (isleaf) {
this._dispatchEvent()
}
if (node) {
this.$emit('nodeclick', node)
}
},
_dispatchEvent() {
this.$emit('change', this.selected.slice(0))
}
}
}
</script>
<style lang="scss">
$uni-primary: #007aff !default;
.uni-data-pickerview {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
overflow: hidden;
height: 100%;
}
.error-text {
color: #DD524D;
}
.loading-cover {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, .5);
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
align-items: center;
z-index: 1001;
}
.load-more {
/* #ifndef APP-NVUE */
margin: auto;
/* #endif */
}
.error-message {
background-color: #fff;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
padding: 15px;
opacity: .9;
z-index: 102;
}
/* #ifdef APP-NVUE */
.selected-area {
width: 750rpx;
}
/* #endif */
.selected-list {
/* #ifndef APP-NVUE */
display: flex;
flex-wrap: nowrap;
/* #endif */
flex-direction: row;
padding: 0 5px;
border-bottom: 1px solid #f8f8f8;
}
.selected-item {
margin-left: 10px;
margin-right: 10px;
padding: 12px 0;
text-align: center;
/* #ifndef APP-NVUE */
white-space: nowrap;
/* #endif */
}
.selected-item-text-overflow {
width: 168px;
/* fix nvue */
overflow: hidden;
/* #ifndef APP-NVUE */
width: 6em;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
/* #endif */
}
.selected-item-active {
border-bottom: 2px solid $uni-primary;
}
.selected-item-text {
color: $uni-primary;
}
.tab-c {
position: relative;
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
overflow: hidden;
}
.list {
flex: 1;
}
.item {
padding: 12px 15px;
/* border-bottom: 1px solid #f0f0f0; */
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
}
.is-disabled {
opacity: .5;
}
.item-text {
/* flex: 1; */
color: #333333;
}
.item-text-overflow {
width: 280px;
/* fix nvue */
overflow: hidden;
/* #ifndef APP-NVUE */
width: 20em;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
/* #endif */
}
.check {
margin-right: 5px;
border: 2px solid $uni-primary;
border-left: 0;
border-top: 0;
height: 12px;
width: 6px;
transform-origin: center;
/* #ifndef APP-NVUE */
transition: all 0.3s;
/* #endif */
transform: rotate(45deg);
}
</style>

View File

@@ -0,0 +1,93 @@
{
"id": "uni-data-picker",
"displayName": "uni-data-picker 数据驱动的picker选择器",
"version": "2.0.2",
"description": "单列、多列级联选择器,常用于省市区城市选择、公司部门选择、多级分类等场景",
"keywords": [
"uni-ui",
"uniui",
"picker",
"级联",
"省市区",
""
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [
"uni-load-more",
"uni-icons",
"uni-scss"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-uvue": "y",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
## DataPicker 级联选择
> **组件名:uni-data-picker**
> 代码块: `uDataPicker`
> 关联组件:`uni-data-pickerview`、`uni-load-more`。
`<uni-data-picker>` 是一个选择类[datacom组件](https://uniapp.dcloud.net.cn/component/datacom)。
支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。
候选数据支持一次性加载完毕,也支持懒加载,比如示例图中,选择了“北京”后,动态加载北京的区县数据。
`<uni-data-picker>` 组件尤其适用于地址选择、分类选择等选择类。
`<uni-data-picker>` 支持本地数据、云端静态数据(json),uniCloud云数据库数据。
`<uni-data-picker>` 可以通过JQL直连uniCloud云数据库,配套[DB Schema](https://uniapp.dcloud.net.cn/uniCloud/schema),可在schema2code中自动生成前端页面,还支持服务器端校验。
在uniCloud数据表中新建表“uni-id-address”和“opendb-city-china”,这2个表的schema自带foreignKey关联。在“uni-id-address”表的表结构页面使用schema2code生成前端页面,会自动生成地址管理的维护页面,自动从“opendb-city-china”表包含的中国所有省市区信息里选择地址。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-picker)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

View File

@@ -0,0 +1,51 @@
## 1.1.0(2025-08-19)
- 新增 插槽 selected empty option
- 新增 mutiple 属性,支持多选功能
- 新增 wrap 属性,支持选中的文字超过一行显示
- 新增 align 属性,支持修改选中的文字显示的位置
- 新增 hideRight 属性,支持隐藏右侧所有按钮
- 新增 mode 属性,支持修改边框样式
- 新增 事件 open close clear
## 1.0.10(2025-04-14)
- 修复 清除按钮不展示问题
## 1.0.9(2025-03-26)
- 优化 默认背景为白色与整体组件保持风格统一
## 1.0.8(2024-03-28)
- 修复 在vue2下:style动态绑定导致编译失败的bug
## 1.0.7(2024-01-20)
- 修复 长文本回显超过容器的bug,超过容器部分显示省略号
## 1.0.6(2023-04-12)
- 修复 微信小程序点击时会改变背景颜色的 bug
## 1.0.5(2023-02-03)
- 修复 禁用时会显示清空按钮
## 1.0.4(2023-02-02)
- 优化 查询条件短期内多次变更只查询最后一次变更后的结果
- 调整 内部缓存键名调整为 uni-data-select-lastSelectedValue
## 1.0.3(2023-01-16)
- 修复 不关联服务空间报错的问题
## 1.0.2(2023-01-14)
- 新增 属性 `format` 可用于格式化显示选项内容
## 1.0.1(2022-12-06)
- 修复 当where变化时,数据不会自动更新的问题
## 0.1.9(2022-09-05)
- 修复 微信小程序下拉框出现后选择会点击到蒙板后面的输入框
## 0.1.8(2022-08-29)
- 修复 点击的位置不准确
## 0.1.7(2022-08-12)
- 新增 支持 disabled 属性
## 0.1.6(2022-07-06)
- 修复 pc端宽度异常的bug
## 0.1.5
- 修复 pc端宽度异常的bug
## 0.1.4(2022-07-05)
- 优化 显示样式
## 0.1.3(2022-06-02)
- 修复 localdata 赋值不生效的 bug
- 新增 支持 uni.scss 修改颜色
- 新增 支持选项禁用(数据选项设置 disabled: true 即禁用)
## 0.1.2(2022-05-08)
- 修复 当 value 为 0 时选择不生效的 bug
## 0.1.1(2022-05-07)
- 新增 记住上次的选项(仅 collection 存在时有效)
## 0.1.0(2022-04-22)
- 初始化

View File

@@ -0,0 +1,837 @@
<template>
<view class="uni-stat__select">
<span v-if="label" class="uni-label-text hide-on-phone">{{label + ':'}}</span>
<view class="uni-stat-box" :class="{'uni-stat__actived': current}">
<view class="uni-select" :class="{'uni-select--disabled':disabled, 'uni-select--wrap': shouldWrap , 'border-default': mode == 'default','border-bottom': mode == 'underline'}">
<view class="uni-select__input-box" @click="toggleSelector" :class="{'uni-select__input-box--wrap': shouldWrap}">
<view v-if="slotSelected" class="slot-content padding-top-bottom" :class="{'uni-select__input-text--wrap': shouldWrap}">
<slot name="selected" :selectedItems="getSelectedItems()"></slot>
</view>
<template v-else>
<view v-if="textShow" class="uni-select__input-text" :class="{'uni-select__input-text--wrap': shouldWrap}">
<view class="padding-top-bottom" :class="'align-'+align">{{textShow}}</view>
</view>
<view v-else class="uni-select__input-text uni-select__input-placeholder" :class="'align-'+align">{{typePlaceholder}}</view>
</template>
<view key="clear-button" v-if="!hideRight && shouldShowClear && clear && !disabled" @click.stop="clearVal">
<uni-icons type="clear" color="#c0c4cc" size="24" />
</view>
<view key="arrow-button" v-else-if="!hideRight">
<uni-icons :type="showSelector? 'top' : 'bottom'" size="14" color="#999" />
</view>
</view>
<view class="uni-select--mask" v-if="showSelector" @click="toggleSelector" />
<view class="uni-select__selector" :style="getOffsetByPlacement" v-if="showSelector">
<view :class="placement=='bottom'?'uni-popper__arrow_bottom':'uni-popper__arrow_top'"></view>
<scroll-view scroll-y="true" class="uni-select__selector-scroll">
<template v-if="slotEmpty && mixinDatacomResData.length === 0">
<view class="uni-select__selector-empty">
<slot name="empty" :empty="emptyTips"></slot>
</view>
</template>
<template v-else>
<view v-if="mixinDatacomResData.length === 0" class="uni-select__selector-empty">
<text>{{emptyTips}}</text>
</view>
</template>
<template v-if="slotOption">
<view v-for="(itemData,index) in mixinDatacomResData" :key="index" @click="change(itemData)">
<slot name="option" :item="itemData" :itemSelected="multiple? getCurrentValues().includes(itemData.value):getCurrentValues() == itemData.value"></slot>
</view>
</template>
<template v-else>
<view v-if="!multiple && mixinDatacomResData.length > 0" class="uni-select__selector-item" v-for="(item,index) in mixinDatacomResData" :key="index"
@click="change(item)">
<text :class="{'uni-select__selector__disabled': item.disable}">{{formatItemName(item)}}</text>
</view>
<view v-if="multiple && mixinDatacomResData.length > 0" >
<checkbox-group @change="checkBoxChange">
<label class="uni-select__selector-item" v-for="(item,index) in mixinDatacomResData" :key="index" >
<checkbox :value="index+''" :checked="getCurrentValues().includes(item.value)" :disabled="item.disable"></checkbox>
<view :class="{'uni-select__selector__disabled': item.disable}">{{formatItemName(item)}}</view>
</label>
</checkbox-group>
</view>
</template>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script>
/**
* DataChecklist 数据选择器
* @description 通过数据渲染的下拉框组件
* @tutorial https://uniapp.dcloud.io/component/uniui/uni-data-select
* @property {String|Array} value 默认值,多选时为数组
* @property {Array} localdata 本地数据 ,格式 [{text:'',value:''}]
* @property {Boolean} clear 是否可以清空已选项
* @property {Boolean} emptyText 没有数据时显示的文字 ,本地数据无效
* @property {String} label 左侧标题
* @property {String} placeholder 输入框的提示文字
* @property {Boolean} disabled 是否禁用
* @property {Boolean} multiple 是否多选模式
* @property {Boolean} wrap 是否允许选中文本换行显示
* @property {String} placement 弹出位置
* @value top 顶部弹出
* @value bottom 底部弹出(default)
* @property {String} align 选择文字的位置
* @value left 显示左侧
* @value center 显示中间
* @value right 显示 右侧
* @property {Boolean} hideRight 是否隐藏右侧按钮
* @property {String} mode 边框样式
* @value default 四周边框
* @value underline 下边框
* @value none 无边框
* @event {Function} change 选中发生变化触发
* @event {Function} open 选择框开启时触发
* @event {Function} close 选择框关闭时触发
* @event {Function} clear 点击清除按钮之后触发
*/
export default {
name: "uni-data-select",
mixins: [uniCloud.mixinDatacom || {}],
emits: [
'open',
'close',
'update:modelValue',
'input',
'clear',
'change'
],
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
options: {
// #ifdef MP-TOUTIAO
virtualHost: false,
// #endif
// #ifndef MP-TOUTIAO
virtualHost: true
// #endif
},
props: {
localdata: {
type: Array,
default () {
return []
}
},
value: {
type: [String, Number, Array],
default: ''
},
modelValue: {
type: [String, Number, Array],
default: ''
},
label: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '请选择'
},
emptyTips: {
type: String,
default: '无选项'
},
clear: {
type: Boolean,
default: true
},
defItem: {
type: Number,
default: 0
},
disabled: {
type: Boolean,
default: false
},
// 格式化输出 用法 field="_id as value, version as text, uni_platform as label" format="{label} - {text}"
format: {
type: String,
default: ''
},
placement: {
type: String,
default: 'bottom'
},
multiple: {
type: Boolean,
default: false
},
wrap: {
type: Boolean,
default: false
},
align:{
type: String,
default: "left"
},
hideRight: {
type: Boolean,
default: false
},
mode:{
type: String,
default: 'default'
}
},
data() {
return {
showSelector: false,
current: '',
mixinDatacomResData: [],
apps: [],
channels: [],
cacheKey: "uni-data-select-lastSelectedValue",
};
},
created() {
this.debounceGet = this.debounce(() => {
this.query();
}, 300);
if (this.collection && !this.localdata.length) {
this.debounceGet();
}
},
computed: {
typePlaceholder() {
const text = {
'opendb-stat-app-versions': '版本',
'opendb-app-channels': '渠道',
'opendb-app-list': '应用'
}
const common = this.placeholder
const placeholder = text[this.collection]
return placeholder ?
common + placeholder :
common
},
valueCom() {
if (this.value === '') return this.modelValue
if (this.modelValue === '') return this.value
return this.value
},
textShow() {
// 长文本显示
if (this.multiple) {
const currentValues = this.getCurrentValues();
if (Array.isArray(currentValues) && currentValues.length > 0) {
const selectedItems = this.mixinDatacomResData.filter(item => currentValues.includes(item.value));
return selectedItems.map(item => this.formatItemName(item)).join(', ');
} else {
return ''; // 空数组时返回空字符串,显示占位符
}
} else {
return this.current;
}
},
shouldShowClear() {
if (this.multiple) {
const currentValues = this.getCurrentValues();
return Array.isArray(currentValues) && currentValues.length > 0;
} else {
return !!this.current;
}
},
shouldWrap() {
// 只有在多选模式、开启换行、且有内容时才应用换行样式
return this.multiple && this.wrap && !!this.textShow;
},
getOffsetByPlacement() {
switch (this.placement) {
case 'top':
return "bottom:calc(100% + 12px);";
case 'bottom':
return "top:calc(100% + 12px);";
}
},
slotSelected(){
// #ifdef VUE2
return this.$scopedSlots ? this.$scopedSlots.selected : false
// #endif
// #ifdef VUE3
return this.$slots ? this.$slots.selected : false
// #endif
},
slotEmpty(){
// #ifdef VUE2
return this.$scopedSlots ? this.$scopedSlots.empty : false
// #endif
// #ifdef VUE3
return this.$slots ? this.$slots.empty : false
// #endif
},
slotOption(){
// #ifdef VUE2
return this.$scopedSlots ? this.$scopedSlots.option : false
// #endif
// #ifdef VUE3
return this.$slots ? this.$slots.option : false
// #endif
}
},
watch: {
showSelector:{
handler(val,old){
val ? this.$emit('open') : this.$emit('close')
}
},
localdata: {
immediate: true,
handler(val, old) {
if (Array.isArray(val) && old !== val) {
this.mixinDatacomResData = val
}
}
},
valueCom(val, old) {
this.initDefVal()
},
mixinDatacomResData: {
immediate: true,
handler(val) {
if (val.length) {
this.initDefVal()
}
}
},
},
methods: {
getSelectedItems() {
const currentValues = this.getCurrentValues();
let _minxData = this.mixinDatacomResData
// #ifdef MP-WEIXIN || MP-TOUTIAO
_minxData = JSON.parse(JSON.stringify(this.mixinDatacomResData))
// #endif
if (this.multiple) {
return _minxData.filter(item => currentValues.includes(item.value)) || [];
} else {
return _minxData.filter(item => item.value === currentValues) || [];
}
},
debounce(fn, time = 100) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, time)
}
},
// 检查项目是否已选中
isSelected(item) {
if (this.multiple) {
const currentValues = this.getCurrentValues();
return Array.isArray(currentValues) && currentValues.includes(item.value);
} else {
return this.getCurrentValues() === item.value;
}
},
// 获取当前选中的值
getCurrentValues() {
if (this.multiple) {
return Array.isArray(this.valueCom) ? this.valueCom : (this.valueCom ? [this.valueCom] : []);
} else {
return this.valueCom;
}
},
// 执行数据库查询
query() {
this.mixinDatacomEasyGet();
},
// 监听查询条件变更事件
onMixinDatacomPropsChange() {
if (this.collection) {
this.debounceGet();
}
},
initDefVal() {
let defValue = this.multiple ? [] : ''
if ((this.valueCom || this.valueCom === 0) && !this.isDisabled(this.valueCom)) {
defValue = this.valueCom
} else {
let strogeValue
if (this.collection) {
strogeValue = this.getCache()
}
if (strogeValue || strogeValue === 0) {
defValue = strogeValue
} else {
let defItem = this.multiple ? [] : ''
if (this.defItem > 0 && this.defItem <= this.mixinDatacomResData.length) {
defItem = this.multiple ? [this.mixinDatacomResData[this.defItem - 1].value] : this.mixinDatacomResData[this.defItem - 1].value
}
defValue = defItem
}
if (defValue || defValue === 0 || (this.multiple && Array.isArray(defValue) && defValue.length > 0)) {
this.emit(defValue)
}
}
if (this.multiple) {
const selectedValues = Array.isArray(defValue) ? defValue : (defValue ? [defValue] : []);
const selectedItems = this.mixinDatacomResData.filter(item => selectedValues.includes(item.value));
this.current = selectedItems.map(item => this.formatItemName(item));
} else {
const def = this.mixinDatacomResData.find(item => item.value === defValue)
this.current = def ? this.formatItemName(def) : ''
}
},
/**
* @param {[String, Number, Array]} value
* 判断用户给的 value 是否同时为禁用状态
*/
isDisabled(value) {
if (Array.isArray(value)) {
// 对于数组,如果任意一个值被禁用,则认为整体被禁用
return value.some(val => {
return this.mixinDatacomResData.some(item => item.value === val && item.disable);
});
} else {
let isDisabled = false;
this.mixinDatacomResData.forEach(item => {
if (item.value === value) {
isDisabled = item.disable
}
})
return isDisabled;
}
},
clearVal() {
const emptyValue = this.multiple ? [] : '';
this.emit(emptyValue)
this.current = this.multiple ? [] : ''
if (this.collection) {
this.removeCache()
}
this.$emit('clear')
},
checkBoxChange(res){
let range = res.detail.value
let currentValues = range && range.length > 0? range.map((item)=>{
const index = parseInt(item, 10);
if (isNaN(index)) {
console.error(`无效索引: ${item}`);
}
if (index < 0 || index >= this.mixinDatacomResData.length) {
console.error(`索引越界: ${index}`);
}
return this.mixinDatacomResData[index].value;
}) : []
const selectedItems = this.mixinDatacomResData.filter(dataItem => currentValues.includes(dataItem.value));
this.current = selectedItems.map(dataItem => this.formatItemName(dataItem));
this.emit(currentValues);
},
change(item) {
if (!item.disable) {
if (this.multiple) {
// 多选模式
let currentValues = this.getCurrentValues();
if (!Array.isArray(currentValues)) {
currentValues = currentValues ? [currentValues] : [];
}
const itemValue = item.value;
const index = currentValues.indexOf(itemValue);
if (index > -1) {
currentValues.splice(index, 1);
} else {
currentValues.push(itemValue);
}
const selectedItems = this.mixinDatacomResData.filter(dataItem => currentValues.includes(dataItem.value));
this.current = selectedItems.map(dataItem => this.formatItemName(dataItem));
this.emit(currentValues);
} else {
// 单选模式
this.showSelector = false
this.current = this.formatItemName(item)
this.emit(item.value)
}
}
},
emit(val) {
this.$emit('input', val)
this.$emit('update:modelValue', val)
this.$emit('change', val)
if (this.collection) {
this.setCache(val);
}
},
toggleSelector() {
if (this.disabled) {
return
}
this.showSelector = !this.showSelector
},
formatItemName(item) {
let {
text,
value,
channel_code
} = item
channel_code = channel_code ? `(${channel_code})` : ''
if (this.format) {
// 格式化输出
let str = "";
str = this.format;
for (let key in item) {
str = str.replace(new RegExp(`{${key}}`, "g"), item[key]);
}
return str;
} else {
return this.collection.indexOf('app-list') > 0 ?
`${text}(${value})` :
(
text ?
text :
`未命名${channel_code}`
)
}
},
// 获取当前加载的数据
getLoadData() {
return this.mixinDatacomResData;
},
// 获取当前缓存key
getCurrentCacheKey() {
return this.collection;
},
// 获取缓存
getCache(name = this.getCurrentCacheKey()) {
let cacheData = uni.getStorageSync(this.cacheKey) || {};
return cacheData[name];
},
// 设置缓存
setCache(value, name = this.getCurrentCacheKey()) {
let cacheData = uni.getStorageSync(this.cacheKey) || {};
cacheData[name] = value;
uni.setStorageSync(this.cacheKey, cacheData);
},
// 删除缓存
removeCache(name = this.getCurrentCacheKey()) {
let cacheData = uni.getStorageSync(this.cacheKey) || {};
delete cacheData[name];
uni.setStorageSync(this.cacheKey, cacheData);
},
}
}
</script>
<style lang="scss">
$uni-base-color: #6a6a6a !default;
$uni-main-color: #333 !default;
$uni-secondary-color: #909399 !default;
$uni-border-3: #e5e5e5;
$uni-primary: #2979ff !default;
$uni-success: #4cd964 !default;
$uni-warning: #f0ad4e !default;
$uni-error: #dd524d !default;
$uni-info: #909399 !default;
/* #ifndef APP-NVUE */
@media screen and (max-width: 500px) {
.hide-on-phone {
display: none;
}
}
/* #endif */
.uni-stat__select {
display: flex;
align-items: center;
// padding: 15px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
width: 100%;
flex: 1;
box-sizing: border-box;
}
.uni-stat-box {
background-color: #fff;
width: 100%;
flex: 1;
}
.uni-stat__actived {
width: 100%;
flex: 1;
// outline: 1px solid #2979ff;
}
.uni-label-text {
font-size: 14px;
font-weight: bold;
color: $uni-base-color;
margin: auto 0;
margin-right: 5px;
}
.border-bottom {
border-bottom: solid 1px $uni-border-3;
}
.border-default {
border: 1px solid $uni-border-3;
}
.uni-select {
font-size: 14px;
box-sizing: border-box;
border-radius: 4px;
padding: 0 5px;
padding-left: 10px;
position: relative;
/* #ifndef APP-NVUE */
display: flex;
user-select: none;
/* #endif */
flex-direction: row;
align-items: center;
width: 100%;
flex: 1;
min-height: 35px;
&--disabled {
background-color: #f5f7fa;
cursor: not-allowed;
}
&--wrap {
height: auto;
min-height: 35px;
// align-items: flex-start;
}
}
.uni-select__label {
font-size: 16px;
// line-height: 22px;
height: 35px;
padding-right: 10px;
color: $uni-secondary-color;
}
.uni-select__input-box {
// height: 35px;
width: 0px;
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
&--wrap {
.uni-select__input-text {
margin-right: 8px;
}
}
.padding-top-bottom {
padding-top: 5px;
padding-bottom: 5px;
}
.slot-content {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
}
.uni-select__input {
flex: 1;
font-size: 14px;
height: 22px;
line-height: 22px;
}
.uni-select__input-plac {
font-size: 14px;
color: $uni-secondary-color;
}
.uni-select__selector {
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
position: absolute;
left: 0;
width: 100%;
background-color: #FFFFFF;
border: 1px solid #EBEEF5;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 3;
padding: 4px 0;
}
.uni-select__selector-scroll {
/* #ifndef APP-NVUE */
max-height: 200px;
box-sizing: border-box;
/* #endif */
}
/* #ifdef H5 */
@media (min-width: 768px) {
.uni-select__selector-scroll {
max-height: 600px;
}
}
/* #endif */
.uni-select__selector-empty,
.uni-select__selector-item {
/* #ifndef APP-NVUE */
display: flex;
cursor: pointer;
/* #endif */
flex-direction: row;
align-items: center;
line-height: 35px;
font-size: 14px;
/* border-bottom: solid 1px $uni-border-3; */
padding: 0px 10px;
}
.uni-select__selector-item-check {
margin-left: auto;
}
.uni-select__selector-empty:last-child,
.uni-select__selector-item:last-child {
/* #ifndef APP-NVUE */
border-bottom: none;
/* #endif */
}
.uni-select__selector__disabled {
opacity: 0.4;
cursor: default;
}
/* picker 弹出层通用的指示小三角 */
.uni-popper__arrow_bottom,
.uni-popper__arrow_bottom::after,
.uni-popper__arrow_top,
.uni-popper__arrow_top::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
}
.uni-popper__arrow_bottom {
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
top: -6px;
left: 10%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #EBEEF5;
}
.uni-popper__arrow_bottom::after {
content: " ";
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
.uni-popper__arrow_top {
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
bottom: -6px;
left: 10%;
margin-right: 3px;
border-bottom-width: 0;
border-top-color: #EBEEF5;
}
.uni-popper__arrow_top::after {
content: " ";
bottom: 1px;
margin-left: -6px;
border-bottom-width: 0;
border-top-color: #fff;
}
.uni-select__input-text {
// width: 280px;
width: 100%;
color: $uni-main-color;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
overflow: hidden;
&--wrap {
white-space: normal;
text-overflow: initial;
-o-text-overflow: initial;
overflow: visible;
word-wrap: break-word;
word-break: break-all;
// line-height: 1.5;
}
}
.uni-select__input-placeholder {
color: $uni-base-color;
font-size: 12px;
margin: 1px 0;
}
.uni-select--mask {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 2;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
</style>

View File

@@ -0,0 +1,106 @@
{
"id": "uni-data-select",
"displayName": "uni-data-select 下拉框选择器",
"version": "1.1.0",
"description": "通过数据驱动的下拉框选择器",
"keywords": [
"uni-ui",
"select",
"uni-data-select",
"下拉框",
"下拉选"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "^3.1.1",
"uni-app": "^4.45",
"uni-app-x": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [
"uni-load-more"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "-",
"android": "√",
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√",
"alipay": "√",
"toutiao": "√",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
## DataSelect 下拉框选择器
> **组件名:uni-data-select**
> 代码块: `uDataSelect`
当选项过多时,使用下拉菜单展示并选择内容
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-select)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

View File

@@ -0,0 +1,10 @@
## 1.0.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-dateformat](https://uniapp.dcloud.io/component/uniui/uni-dateformat)
## 0.0.5(2021-07-08)
- 调整 默认时间不再是当前时间,而是显示'-'字符
## 0.0.4(2021-05-12)
- 新增 组件示例地址
## 0.0.3(2021-02-04)
- 调整为uni_modules目录规范
- 修复 iOS 平台日期格式化出错的问题

View File

@@ -0,0 +1,200 @@
// yyyy-MM-dd hh:mm:ss.SSS 所有支持的类型
function pad(str, length = 2) {
str += ''
while (str.length < length) {
str = '0' + str
}
return str.slice(-length)
}
const parser = {
yyyy: (dateObj) => {
return pad(dateObj.year, 4)
},
yy: (dateObj) => {
return pad(dateObj.year)
},
MM: (dateObj) => {
return pad(dateObj.month)
},
M: (dateObj) => {
return dateObj.month
},
dd: (dateObj) => {
return pad(dateObj.day)
},
d: (dateObj) => {
return dateObj.day
},
hh: (dateObj) => {
return pad(dateObj.hour)
},
h: (dateObj) => {
return dateObj.hour
},
mm: (dateObj) => {
return pad(dateObj.minute)
},
m: (dateObj) => {
return dateObj.minute
},
ss: (dateObj) => {
return pad(dateObj.second)
},
s: (dateObj) => {
return dateObj.second
},
SSS: (dateObj) => {
return pad(dateObj.millisecond, 3)
},
S: (dateObj) => {
return dateObj.millisecond
},
}
// 这都n年了iOS依然不认识2020-12-12,需要转换为2020/12/12
function getDate(time) {
if (time instanceof Date) {
return time
}
switch (typeof time) {
case 'string':
{
// 2020-12-12T12:12:12.000Z、2020-12-12T12:12:12.000
if (time.indexOf('T') > -1) {
return new Date(time)
}
return new Date(time.replace(/-/g, '/'))
}
default:
return new Date(time)
}
}
export function formatDate(date, format = 'yyyy/MM/dd hh:mm:ss') {
if (!date && date !== 0) {
return ''
}
date = getDate(date)
const dateObj = {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hour: date.getHours(),
minute: date.getMinutes(),
second: date.getSeconds(),
millisecond: date.getMilliseconds()
}
const tokenRegExp = /yyyy|yy|MM|M|dd|d|hh|h|mm|m|ss|s|SSS|SS|S/
let flag = true
let result = format
while (flag) {
flag = false
result = result.replace(tokenRegExp, function(matched) {
flag = true
return parser[matched](dateObj)
})
}
return result
}
export function friendlyDate(time, {
locale = 'zh',
threshold = [60000, 3600000],
format = 'yyyy/MM/dd hh:mm:ss'
}) {
if (time === '-') {
return time
}
if (!time && time !== 0) {
return ''
}
const localeText = {
zh: {
year: '年',
month: '月',
day: '天',
hour: '小时',
minute: '分钟',
second: '秒',
ago: '前',
later: '后',
justNow: '刚刚',
soon: '马上',
template: '{num}{unit}{suffix}'
},
en: {
year: 'year',
month: 'month',
day: 'day',
hour: 'hour',
minute: 'minute',
second: 'second',
ago: 'ago',
later: 'later',
justNow: 'just now',
soon: 'soon',
template: '{num} {unit} {suffix}'
}
}
const text = localeText[locale] || localeText.zh
let date = getDate(time)
let ms = date.getTime() - Date.now()
let absMs = Math.abs(ms)
if (absMs < threshold[0]) {
return ms < 0 ? text.justNow : text.soon
}
if (absMs >= threshold[1]) {
return formatDate(date, format)
}
let num
let unit
let suffix = text.later
if (ms < 0) {
suffix = text.ago
ms = -ms
}
const seconds = Math.floor((ms) / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
const months = Math.floor(days / 30)
const years = Math.floor(months / 12)
switch (true) {
case years > 0:
num = years
unit = text.year
break
case months > 0:
num = months
unit = text.month
break
case days > 0:
num = days
unit = text.day
break
case hours > 0:
num = hours
unit = text.hour
break
case minutes > 0:
num = minutes
unit = text.minute
break
default:
num = seconds
unit = text.second
break
}
if (locale === 'en') {
if (num === 1) {
num = 'a'
} else {
unit += 's'
}
}
return text.template.replace(/{\s*num\s*}/g, num + '').replace(/{\s*unit\s*}/g, unit).replace(/{\s*suffix\s*}/g,
suffix)
}

View File

@@ -0,0 +1,88 @@
<template>
<text>{{dateShow}}</text>
</template>
<script>
import {friendlyDate} from './date-format.js'
/**
* Dateformat 日期格式化
* @description 日期格式化组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=3279
* @property {Object|String|Number} date 日期对象/日期字符串/时间戳
* @property {String} locale 格式化使用的语言
* @value zh 中文
* @value en 英文
* @property {Array} threshold 应用不同类型格式化的阈值
* @property {String} format 输出日期字符串时的格式
*/
export default {
name: 'uniDateformat',
props: {
date: {
type: [Object, String, Number],
default () {
return '-'
}
},
locale: {
type: String,
default: 'zh',
},
threshold: {
type: Array,
default () {
return [0, 0]
}
},
format: {
type: String,
default: 'yyyy/MM/dd hh:mm:ss'
},
// refreshRate使用不当可能导致性能问题,谨慎使用
refreshRate: {
type: [Number, String],
default: 0
}
},
data() {
return {
refreshMark: 0
}
},
computed: {
dateShow() {
this.refreshMark
return friendlyDate(this.date, {
locale: this.locale,
threshold: this.threshold,
format: this.format
})
}
},
watch: {
refreshRate: {
handler() {
this.setAutoRefresh()
},
immediate: true
}
},
methods: {
refresh() {
this.refreshMark++
},
setAutoRefresh() {
clearInterval(this.refreshInterval)
if (this.refreshRate) {
this.refreshInterval = setInterval(() => {
this.refresh()
}, parseInt(this.refreshRate))
}
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,88 @@
{
"id": "uni-dateformat",
"displayName": "uni-dateformat 日期格式化",
"version": "1.0.0",
"description": "日期格式化组件,可以将日期格式化为1分钟前、刚刚等形式",
"keywords": [
"uni-ui",
"uniui",
"日期格式化",
"时间格式化",
"格式化时间",
""
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": ["uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More