物流查询

This commit is contained in:
砂糖
2025-07-24 15:45:18 +08:00
parent 5e1afb293e
commit 56409a9340
27 changed files with 3950 additions and 2 deletions

52
api/oa/dict.js Normal file
View File

@@ -0,0 +1,52 @@
import request from "@/util/oaRequest"
// 查询字典数据列表
export function listData(query) {
return request({
url: '/system/dict/data/list',
method: 'get',
params: query
})
}
// 查询字典数据详细
export function getData(dictCode) {
return request({
url: '/system/dict/data/' + dictCode,
method: 'get'
})
}
// 根据字典类型查询字典数据信息
export function getDicts(dictType) {
return request({
url: '/system/dict/data/type/' + dictType,
method: 'get'
})
}
// 新增字典数据
export function addData(data) {
return request({
url: '/system/dict/data',
method: 'post',
data: data
})
}
// 修改字典数据
export function updateData(data) {
return request({
url: '/system/dict/data',
method: 'put',
data: data
})
}
// 删除字典数据
export function delData(dictCode) {
return request({
url: '/system/dict/data/' + dictCode,
method: 'delete'
})
}

51
api/oa/express.js Normal file
View File

@@ -0,0 +1,51 @@
import request from "@/util/oaRequest"
// 查询物流预览列表
export function listExpress(query) {
return request({
url: '/oa/express/list',
method: 'get',
params: query
})
}
// 查询物流预览详细
export function getExpress(expressId) {
return request({
url: '/oa/express/' + expressId,
method: 'get'
})
}
// 查询物流预览详细
export function refreshExpress(expressId) {
return request({
url: '/oa/express/refresh/' + expressId,
method: 'get'
})
}
// 新增物流预览
export function addExpress(data) {
return request({
url: '/oa/express',
method: 'post',
data: data
})
}
// 修改物流预览
export function updateExpress(data) {
return request({
url: '/oa/express',
method: 'put',
data: data
})
}
// 删除物流预览
export function delExpress(expressId) {
return request({
url: '/oa/express/' + expressId,
method: 'delete'
})
}

44
api/oa/expressQuestion.js Normal file
View File

@@ -0,0 +1,44 @@
import request from "@/util/oaRequest"
// 查询快递问题列表
export function listExpressQuestion(query) {
return request({
url: '/oa/expressQuestion/list',
method: 'get',
params: query
})
}
// 查询快递问题详细
export function getExpressQuestion(questionId) {
return request({
url: '/oa/expressQuestion/' + questionId,
method: 'get'
})
}
// 新增快递问题
export function addExpressQuestion(data) {
return request({
url: '/oa/expressQuestion',
method: 'post',
data: data
})
}
// 修改快递问题
export function updateExpressQuestion(data) {
return request({
url: '/oa/expressQuestion',
method: 'put',
data: data
})
}
// 删除快递问题
export function delExpressQuestion(questionId) {
return request({
url: '/oa/expressQuestion/' + questionId,
method: 'delete'
})
}

View File

@@ -0,0 +1,88 @@
<template>
<view>
<picker :value="selectedIndex" :range="options" range-key="dictLabel" @change="onChange">
<view class="picker">
{{ selectedLabel || placeholder }}
</view>
</picker>
</view>
</template>
<script>
import { getDicts } from '@/api/oa/dict.js'
export default {
name: 'DictSelect',
props: {
dictType: {
type: String,
required: true
},
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: String,
default: '请选择'
}
},
data() {
return {
options: [],
selectedIndex: -1
}
},
computed: {
selectedLabel() {
const item = this.options[this.selectedIndex]
return item ? item.dictLabel : ''
}
},
watch: {
value: {
immediate: true,
handler(val) {
this.setSelectedIndex(val)
}
},
dictType: {
immediate: true,
handler() {
this.fetchOptions()
}
}
},
methods: {
async fetchOptions() {
if (!this.dictType) return
const res = await getDicts(this.dictType)
this.options = res.data || []
this.setSelectedIndex(this.value)
},
setSelectedIndex(val) {
const idx = this.options.findIndex(item => item.dictValue == val)
this.selectedIndex = idx
},
onChange(e) {
const idx = e.detail.value
this.selectedIndex = idx
const value = this.options[idx]?.dictValue
this.$emit('input', value)
this.$emit('change', value)
}
}
}
</script>
<style scoped>
.picker {
padding: 10rpx 20rpx;
border: 1rpx solid #eee;
border-radius: 8rpx;
background: #fff;
min-height: 60rpx;
display: flex;
align-items: center;
}
</style>

View File

@@ -0,0 +1,222 @@
<template>
<view>
<!-- 已选用户展示 -->
<view v-if="multi ? selectedUsers.length : selectedUser" class="selected-user">
<template v-if="multi">
<view v-for="user in selectedUsers" :key="user.userId" class="user-tag">
<text>{{ user.nickName }}</text>
<text class="remove-btn" @click="removeUser(user)">×</text>
</view>
</template>
<template v-else>
<text>{{ selectedUser.nickName }}</text>
<text class="remove-btn" @click="clearUser">×</text>
</template>
</view>
<button type="primary" size="mini" @click="openPopup">{{ multi ? '选择人员' : '选择负责人' }}</button>
<uni-popup ref="popup" type="center">
<view class="user-select-popup">
<view class="popup-header">
<text>{{ multi ? '选择人员' : '选择负责人' }}</text>
<text class="close-btn" @click="closePopup">×</text>
</view>
<view style="padding: 16rpx;">
<input
v-model="userSearchKeyword"
placeholder="请输入昵称搜索"
class="form-input"
style="width: 100%;"
/>
</view>
<view style="height: 400rpx; overflow-y: auto;">
<uni-data-checkbox
:multiple="multi"
v-model="innerValue"
:localdata="filteredUserList"
:map="{text:'nickName', value:'userId'}"
selectedColor="#409EFF"
/>
<view v-if="filteredUserList.length === 0" style="text-align:center;color:#999;padding:20rpx;">
暂无匹配用户
</view>
</view>
<view class="popup-footer">
<button type="primary" size="mini" @click="confirmSelect" :disabled="multi ? !innerValue.length : !innerValue">确定</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { selectUser } from '@/api/oa/user';
export default {
name: 'OaUserSelect',
props: {
value: {
type: [String, Number, Array],
default: ''
},
multi: {
type: Boolean,
default: false
}
},
data() {
return {
userList: [],
userSearchKeyword: '',
innerValue: this.multi ? [] : '',
selectedUser: null,
selectedUsers: []
};
},
computed: {
filteredUserList() {
if (!this.userSearchKeyword.trim()) return this.userList;
return this.userList.filter(user =>
user.nickName && user.nickName.includes(this.userSearchKeyword.trim())
);
}
},
watch: {
value: {
immediate: true,
handler(val) {
this.innerValue = this.multi ? (Array.isArray(val) ? val : []) : (val || '');
if (this.multi) {
this.selectedUsers = this.userList.filter(u => this.innerValue.includes(u.userId));
} else {
this.selectedUser = this.userList.find(u => u.userId === this.innerValue) || null;
}
}
},
userList(val) {
if (this.multi) {
this.selectedUsers = val.filter(u => this.innerValue.includes(u.userId));
} else {
this.selectedUser = val.find(u => u.userId === this.innerValue) || null;
}
},
multi(val) {
// 切换单多选时重置
this.innerValue = val ? [] : '';
}
},
methods: {
async openPopup() {
if (!this.userList.length) {
const res = await selectUser({ pageNum: 1, pageSize: 999 });
this.userList = res.rows || [];
}
// innerValue已由watch同步
this.$refs.popup.open();
},
closePopup() {
this.$refs.popup.close();
},
selectUser(user) {
if (this.multi) {
const idx = this.selectedUsers.findIndex(u => u.userId === user.userId);
if (idx > -1) {
this.selectedUsers.splice(idx, 1);
} else {
this.selectedUsers.push(user);
}
} else {
this.tempUser = user;
}
},
isSelected(user) {
if (this.multi) {
return this.selectedUsers.some(u => u.userId === user.userId);
} else {
return this.value === user.userId;
}
},
confirmSelect() {
this.$emit('input', this.innerValue);
if (this.multi) {
this.selectedUsers = this.userList.filter(u => this.innerValue.includes(u.userId));
} else {
this.selectedUser = this.userList.find(u => u.userId === this.innerValue) || null;
}
this.closePopup();
},
clearUser() {
this.$emit('input', this.multi ? [] : '');
if (this.multi) {
this.selectedUsers = [];
} else {
this.selectedUser = null;
}
},
removeUser(user) {
if (this.multi) {
this.innerValue = this.innerValue.filter(id => id !== user.userId);
this.selectedUsers = this.selectedUsers.filter(u => u.userId !== user.userId);
this.$emit('input', this.innerValue);
}
}
}
};
</script>
<style scoped>
.selected-user {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 8rpx;
}
.remove-btn {
color: #ff4757;
margin-left: 8rpx;
font-size: 28rpx;
cursor: pointer;
}
.user-select-popup {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
width: 600rpx;
max-width: 90vw;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 30rpx;
font-weight: bold;
margin-bottom: 16rpx;
}
.close-btn {
color: #888;
font-size: 36rpx;
cursor: pointer;
}
.user-list {
margin-top: 8rpx;
}
.user-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 0;
border-bottom: 1rpx solid #f0f0f0;
cursor: pointer;
}
.user-row.selected {
background: #e6fffb;
}
.user-dept {
color: #999;
font-size: 24rpx;
margin-left: auto;
}
.popup-footer {
display: flex;
justify-content: flex-end;
margin-top: 16rpx;
}
</style>

View File

@@ -309,6 +309,14 @@
"navigationBarTitleText" : "项目排产",
"navigationStyle": "default"
}
},
{
"path" : "pages/workbench/express/express",
"style" :
{
"navigationBarTitleText" : "快递信息",
"navigationStyle": "default"
}
}
],
"tabBar": {

View File

@@ -10,7 +10,7 @@
<view class="add-button-container">
<u-button
type="primary"
@click="showAddForm"
@click="showAddForm"
:custom-style="{ borderRadius: '50rpx', height: '80rpx' }"
>
<u-icon name="plus" color="#fff" size="20" style="margin-right: 10rpx;"></u-icon>

View File

@@ -0,0 +1,55 @@
<template>
<view class="remain-span">
<view v-if="status === 2">
<uni-tag text="已完成" type="success" />
</view>
<view v-else-if="status === 3">
<uni-tag text="异常" type="error" />
</view>
<template v-else>
<view v-if="isTimeout">
<uni-tag text="超时" type="error" />
</view>
<view v-else>
<uni-tag :text="'剩余' + remainDays + '天'" type="info" />
</view>
</template>
</view>
</template>
<script>
export default {
name: 'ExpressRemainTime',
props: {
planDate: {
type: [String, Date],
required: false
},
status: {
type: Number,
required: true
}
},
computed: {
remainDays() {
if (!this.planDate) return '-';
const now = new Date();
const plan = new Date(this.planDate);
const diff = plan.getTime() - now.getTime();
return Math.ceil(diff / (1000 * 60 * 60 * 24));
},
isTimeout() {
if (!this.planDate) return false;
const now = new Date();
const plan = new Date(this.planDate);
return now.getTime() > plan.getTime();
}
}
}
</script>
<style scoped>
.remain-span {
display: inline-block;
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<view>
<view v-if="!isCustomExpress" style="color: #606266;">
<text>{{ lastUpdateTime || '-' }}</text>
<view v-if="lastStatus" class="ellipsis" style="max-width: 400rpx; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; font-size: 26rpx;">
{{ lastStatus }}
</view>
<view v-else style="font-size: 26rpx; color: #999; display: inline-block;">未发货或无法获取</view>
</view>
<view v-else>
<text @click="openDialog" style="color:#409EFF;text-decoration:underline;">{{ lastUpdateTime || '点击填写' }}</text>
<text @click="openDialog" style="color:#409EFF;text-decoration:underline; margin-left: 16rpx;">{{ lastStatus || '点击填写' }}</text>
</view>
<uni-popup ref="statusPopup" type="bottom">
<view class="popup-content popup-bottom">
<view class="form-row">
<text>物流状态</text>
<uni-easyinput v-model="form.lastStatus" placeholder="请输入物流状态" />
</view>
<view class="form-row">
<text>更新时间</text>
<uni-datetime-picker v-model="form.lastUpdateTime" type="datetime" :clear-icon="false" placeholder="请选择更新时间" />
</view>
<view class="popup-actions">
<uni-button @click="closeDialog">取消</uni-button>
<uni-button type="primary" @click="handleSave">确定</uni-button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { EExpressType } from '@/util/enums';
export default {
name: 'ExpressStatusEditor',
props: {
lastStatus: String,
lastUpdateTime: String,
expressType: String
},
data() {
return {
form: {
lastStatus: '',
lastUpdateTime: ''
},
EExpressType
}
},
computed: {
isCustomExpress() {
if (!this.EExpressType || typeof this.EExpressType !== 'object') {
return true;
}
const expressTypeList = Object.values(this.EExpressType);
return !expressTypeList.includes(this.expressType);
}
},
methods: {
openDialog() {
this.form.lastStatus = this.lastStatus || '';
this.form.lastUpdateTime = this.lastUpdateTime || '';
this.$refs.statusPopup.open();
},
closeDialog() {
this.$refs.statusPopup.close();
},
handleSave() {
if (!this.form.lastStatus || !this.form.lastUpdateTime) {
uni.showToast({ title: '请填写完整物流状态和更新时间', icon: 'none' });
return;
}
this.$emit('update:lastStatus', this.form.lastStatus);
this.$emit('update:lastUpdateTime', this.form.lastUpdateTime);
this.$emit('save', {
lastStatus: this.form.lastStatus,
lastUpdateTime: this.form.lastUpdateTime
});
this.closeDialog();
}
}
}
</script>
<style scoped>
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.popup-content { padding: 20rpx; }
.popup-bottom {
width: 100vw;
background: #fff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
box-sizing: border-box;
}
.form-row { display: flex; align-items: center; margin-bottom: 16rpx; }
.form-row text { width: 120rpx; }
.popup-actions { display: flex; justify-content: flex-end; gap: 20rpx; margin-top: 20rpx; }
</style>

View File

@@ -0,0 +1,669 @@
<template>
<view class="express-page">
<!-- 顶部搜索与新增 -->
<view class="search-bar">
<view class="search-container">
<view class="search-input">
<u-search v-model="searchKeyword" placeholder="搜索物流编号" @confirm="onSearch" @keyup.enter="onSearch"
class="custom-search-input" :show-action="false" />
</view>
<view class="add-button" @click="handleAdd">
<u-icon name="plus" :color="$im-primary" size="18"></u-icon>
</view>
</view>
</view>
<!-- 列表区 -->
<scroll-view scroll-y class="express-list" @scrolltolower="loadMore">
<view v-for="item in expressList" :key="item.expressId" class="express-card">
<!-- 第一行物流公司单号状态 -->
<view class="card-header new-header">
<text class="express-company">{{ item.expressType || '-' }}</text>
<text class="express-code">{{ item.expressCode || '-' }}</text>
<text class="status" :class="'status-' + item.status">{{ statusText(item.status) }}</text>
</view>
<!-- 第二行发货方向对方->负责人及联系方式 -->
<view class="row row-between row-arrow">
<view class="row-party">
<text class="party-label">{{ item.supplyName || '-' }}</text>
<text class="party-phone">{{ item.supplyPhone || '-' }}</text>
</view>
<text class="arrow"></text>
<view class="row-party">
<text class="party-label">{{ item.ownerName || '-' }}</text>
<text class="party-phone">{{ item.ownerPhone || '-' }}</text>
</view>
</view>
<!-- 第三行计划到货时间和剩余天数 -->
<view class="row row-between">
<text class="plan-date">计划到货{{ item.planDate || '-' }}</text>
<ExpressRemainTime :planDate="item.planDate" :status="item.status" />
</view>
<!-- 第四行物流状态编辑器 -->
<view class="row">
<ExpressStatusEditor :lastStatus.sync="item.lastStatus" :lastUpdateTime.sync="item.lastUpdateTime"
:expressType="item.expressType" @save="val => handleStatusSave(item, val)" />
</view>
<!-- 第五行备注 -->
<view class="row remark-row">
<text class="remark-label">备注</text>
<text class="remark-content">{{ item.remark || '-' }}</text>
</view>
<!-- 第六行操作按钮 -->
<view class="card-actions new-actions">
<!-- 未确认确认修改 -->
<template v-if="item.status === 0">
<view class="action-btn action-confirm" @click="handleUpdateStatus(item, 1)">确认</view>
<view class="action-btn action-edit" @click="handleUpdate(item)">修改</view>
</template>
<!-- 进行中完成同步异常 -->
<template v-else-if="item.status === 1">
<view class="action-btn action-finish" @click="handleUpdateStatus(item, 2)">完成</view>
<view class="action-btn action-sync" @click="handleRefresh(item)">同步</view>
<view class="action-btn action-exception" @click="handleUpdateStatus(item, 3)">异常</view>
</template>
<!-- 已完成无操作按钮仅显示删除 -->
<template v-else-if="item.status === 2">
<!-- -->
</template>
<!-- 异常同步 -->
<template v-else-if="item.status === 3">
<view class="action-btn action-sync" @click="handleRefresh(item)">同步</view>
</template>
<!-- 编辑和删除按钮始终显示 -->
<view class="action-btn action-edit" @click="handleUpdate(item)">编辑</view>
<view class="action-btn action-delete" @click="handleDelete(item)">删除</view>
</view>
</view>
<view v-if="loadingMore" class="loading-more">加载中...</view>
<view v-if="noMore" class="no-more">没有更多了</view>
</scroll-view>
<!-- 新增/编辑弹窗 -->
<uni-popup ref="editPopup" type="bottom">
<view class="popup-content popup-bottom">
<view class="form-row">
<text>物流编号</text>
<uni-easyinput v-model="form.expressCode" placeholder="请输入物流编号" />
</view>
<view class="form-row">
<text>对方姓名</text>
<uni-easyinput v-model="form.supplyName" placeholder="请输入对方姓名" />
</view>
<view class="form-row">
<text>对方手机</text>
<uni-easyinput v-model="form.supplyPhone" placeholder="请输入对方手机" />
</view>
<view class="form-row">
<text>负责人</text>
<oa-user-select v-model="form.ownerId" />
</view>
<view class="form-row">
<text>负责人手机</text>
<uni-easyinput v-model="form.ownerPhone" placeholder="请输入负责人手机" />
</view>
<view class="form-row">
<text>计划到货</text>
<uni-easyinput v-model="form.planDate" placeholder="请输入计划到货时间" />
</view>
<view class="form-row">
<text>物流公司</text>
<oa-dict-select v-model="form.expressType" :dictType="'oa_express_type'" placeholder="请选择物流公司" />
</view>
<view class="form-row">
<text>备注</text>
<uni-easyinput v-model="form.remark" placeholder="请输入备注" />
</view>
<view class="popup-actions">
<uni-button type="primary" @click="submitForm">保存</uni-button>
<uni-button @click="cancel">取消</uni-button>
</view>
</view>
</uni-popup>
<!-- 异常登记弹窗 -->
<uni-popup ref="exceptionPopup" type="bottom">
<view class="popup-content popup-bottom">
<view class="form-row">
<text>问题描述</text>
<uni-easyinput v-model="exceptionForm.description" placeholder="请输入问题描述" />
</view>
<view class="form-row">
<text>汇报时间</text>
<uni-easyinput v-model="exceptionForm.reportTime" placeholder="请选择汇报时间" />
</view>
<view class="form-row">
<text>备注</text>
<uni-easyinput v-model="exceptionForm.remark" placeholder="请输入备注" />
</view>
<view class="popup-actions">
<uni-button type="primary" @click="submitException"> </uni-button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { addExpress, delExpress, getExpress, listExpress, updateExpress } from '@/api/oa/express';
import ExpressStatusEditor from './components/ExpressStatusEditor.vue';
import ExpressRemainTime from './components/ExpressRemainTime.vue';
import { addExpressQuestion } from '@/api/oa/expressQuestion';
export default {
components: {
ExpressStatusEditor,
ExpressRemainTime,
},
data() {
return {
expressList: [],
pageNum: 1,
pageSize: 10,
total: 0,
loadingMore: false,
noMore: false,
form: {
ownerUser: {},
},
expressDetail: {},
loading: false,
exceptionForm: {
description: '',
reportTime: '',
remark: ''
},
currentExceptionItem: null,
};
},
onLoad() {
this.getList();
},
methods: {
handleStatusSave(row, val) {
row.lastStatus = val.lastStatus;
row.lastUpdateTime = val.lastUpdateTime;
updateExpress(row).then(() => {
uni.showToast({ title: '保存成功', icon: 'success' });
this.getList();
});
},
getList(isLoadMore = false) {
if (this.loadingMore) return;
this.loadingMore = true;
listExpress({ pageNum: this.pageNum, pageSize: this.pageSize }).then(res => {
const rows = res.rows || [];
if (isLoadMore) {
this.expressList = this.expressList.concat(rows);
} else {
this.expressList = rows;
}
this.total = res.total || 0;
this.noMore = rows.length < this.pageSize;
}).finally(() => {
this.loadingMore = false;
});
},
loadMore() {
if (this.noMore || this.loadingMore) return;
this.pageNum++;
this.getList(true);
},
handleAdd() {
this.form = { ownerUser: {} };
this.$refs.editPopup.open();
},
handleUpdate(item) {
getExpress(item.expressId).then(res => {
const data = res.data || {};
this.form = {
...data,
ownerUser: data.ownerUser || {},
};
this.$refs.editPopup.open();
});
},
handleDelete(item) {
uni.showModal({
title: '提示',
content: '确定要删除该物流单吗?',
success: (res) => {
if (res.confirm) {
delExpress(item.expressId).then(() => {
uni.showToast({ title: '删除成功', icon: 'success' });
this.pageNum = 1;
this.getList();
});
}
}
});
},
submitForm() {
// 提交时将负责人信息拆分到 ownerName/ownerPhone 字段
const submitData = { ...this.form };
if (submitData.expressId) {
updateExpress(submitData).then(() => {
uni.showToast({ title: '修改成功', icon: 'success' });
this.$refs.editPopup.close();
this.pageNum = 1;
this.getList();
});
} else {
addExpress(submitData).then(() => {
uni.showToast({ title: '新增成功', icon: 'success' });
this.$refs.editPopup.close();
this.pageNum = 1;
this.getList();
});
}
},
cancel() {
this.$refs.editPopup.close();
},
handleDetail(item) {
getExpress(item.expressId).then(res => {
this.expressDetail = res.data || {};
this.$refs.detailPopup.open();
});
},
closeDetailPopup() {
this.$refs.detailPopup.close();
},
handleUpdateStatus(item, status) {
if (status === 3) {
this.currentExceptionItem = item;
this.$refs.exceptionPopup.open();
this.exceptionForm = {
description: '',
reportTime: this.formatDate(new Date()),
remark: ''
};
return;
}
item.status = status;
updateExpress(item).then(() => {
uni.showToast({ title: '状态更新成功', icon: 'success' });
this.getList();
});
},
submitException() {
const item = this.currentExceptionItem;
addExpressQuestion({
expressId: item.expressId,
description: this.exceptionForm.description,
reportTime: this.exceptionForm.reportTime,
remark: this.exceptionForm.remark
}).then(() => {
item.status = 3;
return updateExpress(item);
}).then(() => {
uni.showToast({ title: '异常登记成功', icon: 'success' });
this.getList();
}).finally(() => {
this.$refs.exceptionPopup.close();
this.exceptionForm = {
description: '',
reportTime: '',
remark: ''
};
this.currentExceptionItem = null;
});
},
formatDate(date) {
const pad = n => n < 10 ? '0' + n : n;
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' '
+ pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
},
handleRefresh(item) {
// 实现同步逻辑
uni.showToast({ title: '同步成功', icon: 'success' });
this.getList();
},
statusText(status) {
switch (status) {
case 0: return '未确认';
case 1: return '进行中';
case 2: return '已完成';
case 3: return '异常';
default: return '-';
}
},
}
};
</script>
<style scoped>
.express-page {
padding: 20rpx;
}
.express-list {
max-height: 100vh;
}
.express-card {
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx #eee;
padding: 20rpx;
}
.card-header {
display: none;
}
.new-header {
display: flex;
align-items: center;
gap: 20rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.express-company {
color: #409EFF;
font-size: 30rpx;
font-weight: 500;
}
.express-code {
color: #333;
font-size: 28rpx;
}
.status {
margin-left: auto;
font-size: 26rpx;
font-weight: 500;
}
.status-0 {
color: #faad14;
}
.status-1 {
color: #1890ff;
}
.status-2 {
color: #52c41a;
}
.status-3 {
color: #f5222d;
}
.row {
display: flex;
align-items: center;
margin: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
padding-bottom: 8rpx;
}
.express-card .row:last-of-type {
border-bottom: none;
}
.row-between {
justify-content: space-between;
align-items: center;
}
.row-arrow {
gap: 16rpx;
}
.row-party {
display: flex;
flex-direction: column;
gap: 2rpx;
}
.party-label {
color: #333;
font-size: 26rpx;
font-weight: 500;
}
.party-phone {
color: #888;
font-size: 24rpx;
}
.arrow {
color: #bbb;
font-size: 32rpx;
margin: 0 8rpx;
}
.plan-date {
color: #666;
font-size: 24rpx;
}
.remark-row {
background: #f7f7f7;
border-radius: 8rpx;
padding: 8rpx 12rpx;
margin-top: 6rpx;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 16rpx;
padding-bottom: 8rpx;
}
.remark-label {
color: #999;
font-size: 24rpx;
}
.remark-content {
color: #333;
font-size: 24rpx;
}
.card-actions {
display: none;
}
.new-actions {
display: flex;
justify-content: flex-end;
gap: 16rpx;
margin-top: 12rpx;
}
.action-btn {
padding: 8rpx 28rpx;
border-radius: 8rpx;
font-size: 26rpx;
font-weight: 500;
cursor: pointer;
margin-left: 0;
transition: background 0.2s;
}
.action-confirm {
background: #f6ffed;
color: #52c41a;
border: 1rpx solid #b7eb8f;
}
.action-confirm:active {
background: #d9f7be;
}
.action-finish {
background: #e6f7ff;
color: #1890ff;
border: 1rpx solid #91d5ff;
}
.action-finish:active {
background: #bae7ff;
}
.action-sync {
background: #fffbe6;
color: #faad14;
border: 1rpx solid #ffe58f;
}
.action-sync:active {
background: #fff1b8;
}
.action-exception {
background: #fff7e6;
color: #fa541c;
border: 1rpx solid #ffd591;
}
.action-exception:active {
background: #ffe7ba;
}
.action-edit {
background: #f6ffed;
color: #389e0d;
border: 1rpx solid #b7eb8f;
}
.action-edit:active {
background: #d9f7be;
}
.action-detail {
background: #f0f5ff;
color: #2f54eb;
border: 1rpx solid #adc6ff;
}
.action-detail:active {
background: #d6e4ff;
}
/* 删除按钮:红色文字,边框和背景更警示 */
.action-delete {
background: #fff1f0;
color: #f5222d;
border: 1rpx solid #ffa39e;
}
.action-delete:active {
background: #ffccc7;
}
.loading-more,
.no-more {
text-align: center;
color: #888;
padding: 20rpx 0;
}
.popup-content {
padding: 20rpx;
}
.popup-bottom {
width: 100vw;
background: #fff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
box-sizing: border-box;
z-index: 9999;
position: relative;
}
.form-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.form-row text {
width: 120rpx;
}
.popup-actions {
display: flex;
justify-content: flex-end;
gap: 20rpx;
margin-top: 20rpx;
}
.detail-row {
display: flex;
margin-bottom: 10rpx;
}
/* 新增按钮样式 */
.add-btn-wrapper {
position: fixed;
left: 0;
right: 0;
bottom: 60rpx;
display: flex;
justify-content: center;
z-index: 100;
}
.add-btn {
width: 100rpx;
height: 100rpx;
background: #409EFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx #b3d8ff;
cursor: pointer;
transition: background 0.2s;
}
.add-btn:active {
background: #66b1ff;
}
.add-btn .iconfont {
color: #fff;
font-size: 60rpx;
font-weight: bold;
line-height: 1;
}
.search-bar {
padding: 20rpx;
position: sticky;
top: 0;
z-index: 100;
position: relative;
}
.search-container {
display: flex;
align-items: center;
gap: 20rpx;
}
.task-type-button-container {
position: relative;
}
.search-input {
flex: 1;
}
.task-type-button,
.add-button {
width: 60rpx;
height: 60rpx;
background-color: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -22,6 +22,10 @@
<image class="entry-icon" src="/static/images/paichan.png" mode="aspectFit"></image>
<text class="entry-text">项目排产</text>
</view>
<view class="entry-item" @click="goExpress">
<image class="entry-icon" src="/static/images/express.svg" mode="aspectFit"></image>
<text class="entry-text">快递信息</text>
</view>
</view>
</view>
</template>
@@ -72,7 +76,12 @@ export default {
uni.navigateTo({
url: '/pages/workbench/reportSchedule/reportSchedule'
})
}
},
goExpress() {
uni.navigateTo({
url: '/pages/workbench/express/express'
})
}
},
};
</script>

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1753335097606" class="icon" viewBox="0 0 1029 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7822" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.9765625" height="200"><path d="M16.911643 290.592766l463.379026 170.21569v538.957159L16.911643 829.549926V290.592766M0 266.358382v574.995871l497.202312 182.645747v-574.995871L0 266.358382zM220.409447 146.015128l440.396102 186.028076-142.463683 53.998877L62.167201 213.796994l158.259157-67.781866m0-18.382956L16.911643 214.77787l501.430223 189.410404 187.92218-71.231841L220.375623 127.632172zM518.324955 18.399868l454.635706 195.397126-130.946854 49.635673L389.086177 73.768588l129.238778-55.351808M518.341866 0L345.724723 73.937704l495.984674 207.691891L1018.080925 214.77787 518.341866 0z" fill="#1A1A1A" p-id="7823"></path><path d="M368.876763 82.410438l450.35706 189.647167-136.98431 51.901833-439.702725-187.41483 126.38071-54.117259m0-18.366044L199.455921 136.595343l482.438447 205.594847 182.865599-69.337737L368.826028 64.044393zM1013.007432 290.592766v538.95716L549.628406 999.765615V460.808456l161.252519-59.190752V571.613543a16.911643 16.911643 0 0 0 23.896152 15.406507l126.837324-57.499588a16.911643 16.911643 0 0 0 9.927135-15.406507V342.562246L1013.007432 290.592766m16.911643-24.234384l-175.323006 64.399537V514.113955l-126.837324 57.499588V377.349496L532.716763 449.004129v574.995871l497.202312-182.645747v-574.995871z" fill="#1A1A1A" p-id="7824"></path><path d="M856.25341 348.176912l-1.572782 166.596597-126.465269 56.992238-1.572783-176.033294 129.610834-47.623188m17.148407-24.318943l-163.772354 60.171627 1.911016 213.813906 159.967234-72.094336 1.911015-201.874285z" fill="#1A1A1A" p-id="7825"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,51 @@
## 1.0.62024-10-22
- 新增 当 multiple 为 false 且传递的 value 为 数组时,使用数组第一项用作反显
## 1.0.52024-03-20
- 修复 单选模式下选中样式不生效的bug
## 1.0.42024-01-27
- 修复 修复错别字chagne为change
## 1.0.32022-09-16
- 可以使用 uni-scss 控制主题色
## 1.0.22022-06-30
- 优化 在 uni-forms 中的依赖注入方式
## 1.0.12022-02-07
- 修复 multiple 为 true 时v-model 的值为 null 报错的 bug
## 1.0.02021-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.52021-08-23
- 修复 在uni-forms中 modelValue 中不存在当前字段,当前字段必填写也不参与校验的问题
## 0.2.42021-08-17
- 修复 单选 list 模式下 icon 为 left 时,选中图标不显示的问题
## 0.2.32021-08-11
- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题
## 0.2.22021-07-30
- 优化 在uni-forms组件与label不对齐的问题
## 0.2.12021-07-27
- 修复 单选默认值为0不能选中的Bug
## 0.2.02021-07-13
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 0.1.112021-07-06
- 优化 删除无用日志
## 0.1.102021-07-05
- 修复 由 0.1.9 引起的非 nvue 端图标不显示的问题
## 0.1.92021-07-05
- 修复 nvue 黑框样式问题
## 0.1.82021-06-28
- 修复 selectedTextColor 属性不生效的Bug
## 0.1.72021-06-02
- 新增 map 属性可以方便映射text/value属性
## 0.1.62021-05-26
- 修复 不关联服务空间的情况下组件报错的Bug
## 0.1.52021-05-12
- 新增 组件示例地址
## 0.1.42021-04-09
- 修复 nvue 下无法选中的问题
## 0.1.32021-03-22
- 新增 disabled属性
## 0.1.22021-02-24
- 优化 默认颜色显示
## 0.1.12021-02-24
- 新增 支持nvue
## 0.1.02021-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,115 @@
## 1.1.192024-07-18
- 修复 初始值传入 null 导致input报错的bug
## 1.1.182024-04-11
- 修复 easyinput组件双向绑定问题
## 1.1.172024-03-28
- 修复 在头条小程序下丢失事件绑定的问题
## 1.1.162024-03-20
- 修复 在密码输入情况下 清除和小眼睛覆盖bug 在edge浏览器下显示双眼睛bug
## 1.1.152024-02-21
- 新增 左侧插槽left
## 1.1.142024-02-19
- 修复 onBlur的emit传值错误
## 1.1.122024-01-29
- 补充 adjust-position文档属性补充
## 1.1.112024-01-29
- 补充 adjust-position属性传递值Boolean当键盘弹起时是否自动上推页面
## 1.1.102024-01-22
- 去除 移除无用的log输出
## 1.1.92023-04-11
- 修复 vue3 下 keyboardheightchange 事件报错的bug
## 1.1.82023-03-29
- 优化 trim 属性默认值
## 1.1.72023-03-29
- 新增 cursor-spacing 属性
## 1.1.62023-01-28
- 新增 keyboardheightchange 事件,可监听键盘高度变化
## 1.1.52022-11-29
- 优化 主题样式
## 1.1.42022-10-27
- 修复 props 中背景颜色无默认值的bug
## 1.1.02022-06-30
- 新增 在 uni-forms 1.4.0 中使用可以在 blur 时校验内容
- 新增 clear 事件,点击右侧叉号图标触发
- 新增 change 事件 ,仅在输入框失去焦点或用户按下回车时触发
- 优化 组件样式,组件获取焦点时高亮显示,图标颜色调整等
## 1.0.52022-06-07
- 优化 clearable 显示策略
## 1.0.42022-06-07
- 优化 clearable 显示策略
## 1.0.32022-05-20
- 修复 关闭图标某些情况下无法取消的 bug
## 1.0.22022-04-12
- 修复 默认值不生效的 bug
## 1.0.12022-04-02
- 修复 value 不能为 0 的 bug
## 1.0.02021-11-19
- 优化 组件 UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-easyinput](https://uniapp.dcloud.io/component/uniui/uni-easyinput)
## 0.1.42021-08-20
- 修复 在 uni-forms 的动态表单中默认值校验不通过的 bug
## 0.1.32021-08-11
- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题
## 0.1.22021-07-30
- 优化 vue3 下事件警告的问题
## 0.1.1
- 优化 errorMessage 属性支持 Boolean 类型
## 0.1.02021-07-13
- 组件兼容 vue3如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 0.0.162021-06-29
- 修复 confirmType 属性(仅 type="text" 生效)导致多行文本框无法换行的 bug
## 0.0.152021-06-21
- 修复 passwordIcon 属性拼写错误的 bug
## 0.0.142021-06-18
- 新增 passwordIcon 属性,当 type=password 时是否显示小眼睛图标
- 修复 confirmType 属性不生效的问题
## 0.0.132021-06-04
- 修复 disabled 状态可清出内容的 bug
## 0.0.122021-05-12
- 新增 组件示例地址
## 0.0.112021-05-07
- 修复 input-border 属性不生效的问题
## 0.0.102021-04-30
- 修复 ios 遮挡文字、显示一半的问题
## 0.0.92021-02-05
- 调整为 uni_modules 目录规范
- 优化 兼容 nvue 页面

View File

@@ -0,0 +1,54 @@
/**
* @desc 函数防抖
* @param func 目标函数
* @param wait 延迟执行毫秒数
* @param immediate true - 立即执行, false - 延迟执行
*/
export const debounce = function(func, wait = 1000, immediate = true) {
let timer;
return function() {
let context = this,
args = arguments;
if (timer) clearTimeout(timer);
if (immediate) {
let callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timer = setTimeout(() => {
func.apply(context, args);
}, wait)
}
}
}
/**
* @desc 函数节流
* @param func 函数
* @param wait 延迟执行毫秒数
* @param type 1 使用表时间戳,在时间段开始的时候触发 2 使用表定时器,在时间段结束的时候触发
*/
export const throttle = (func, wait = 1000, type = 1) => {
let previous = 0;
let timeout;
return function() {
let context = this;
let args = arguments;
if (type === 1) {
let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
} else if (type === 2) {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}

View File

@@ -0,0 +1,676 @@
<template>
<view class="uni-easyinput" :class="{ 'uni-easyinput-error': msg }" :style="boxStyle">
<view class="uni-easyinput__content" :class="inputContentClass" :style="inputContentStyle">
<uni-icons v-if="prefixIcon" class="content-clear-icon" :type="prefixIcon" color="#c0c4cc" @click="onClickIcon('prefix')" size="22"></uni-icons>
<slot name="left">
</slot>
<!-- #ifdef MP-ALIPAY -->
<textarea :enableNative="enableNative" v-if="type === 'textarea'" class="uni-easyinput__content-textarea" :class="{ 'input-padding': inputBorder }" :name="name" :value="val" :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" placeholder-class="uni-easyinput__placeholder-class" :maxlength="inputMaxlength" :focus="focused" :autoHeight="autoHeight" :cursor-spacing="cursorSpacing" :adjust-position="adjustPosition" @input="onInput" @blur="_Blur" @focus="_Focus" @confirm="onConfirm" @keyboardheightchange="onkeyboardheightchange"></textarea>
<input :enableNative="enableNative" v-else :type="type === 'password' ? 'text' : type" class="uni-easyinput__content-input" :style="inputStyle" :name="name" :value="val" :password="!showPassword && type === 'password'" :placeholder="placeholder" :placeholderStyle="placeholderStyle" placeholder-class="uni-easyinput__placeholder-class" :disabled="disabled" :maxlength="inputMaxlength" :focus="focused" :confirmType="confirmType" :cursor-spacing="cursorSpacing" :adjust-position="adjustPosition" @focus="_Focus" @blur="_Blur" @input="onInput" @confirm="onConfirm" @keyboardheightchange="onkeyboardheightchange" />
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<textarea v-if="type === 'textarea'" class="uni-easyinput__content-textarea" :class="{ 'input-padding': inputBorder }" :name="name" :value="val" :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" placeholder-class="uni-easyinput__placeholder-class" :maxlength="inputMaxlength" :focus="focused" :autoHeight="autoHeight" :cursor-spacing="cursorSpacing" :adjust-position="adjustPosition" @input="onInput" @blur="_Blur" @focus="_Focus" @confirm="onConfirm" @keyboardheightchange="onkeyboardheightchange"></textarea>
<input v-else :type="type === 'password' ? 'text' : type" class="uni-easyinput__content-input" :style="inputStyle" :name="name" :value="val" :password="!showPassword && type === 'password'" :placeholder="placeholder" :placeholderStyle="placeholderStyle" placeholder-class="uni-easyinput__placeholder-class" :disabled="disabled" :maxlength="inputMaxlength" :focus="focused" :confirmType="confirmType" :cursor-spacing="cursorSpacing" :adjust-position="adjustPosition" @focus="_Focus" @blur="_Blur" @input="onInput" @confirm="onConfirm" @keyboardheightchange="onkeyboardheightchange" />
<!-- #endif -->
<template v-if="type === 'password' && passwordIcon">
<!-- 开启密码时显示小眼睛 -->
<uni-icons v-if="isVal" class="content-clear-icon" :class="{ 'is-textarea-icon': type === 'textarea' }" :type="showPassword ? 'eye-slash-filled' : 'eye-filled'" :size="22" :color="focusShow ? primaryColor : '#c0c4cc'" @click="onEyes"></uni-icons>
</template>
<template v-if="suffixIcon">
<uni-icons v-if="suffixIcon" class="content-clear-icon" :type="suffixIcon" color="#c0c4cc" @click="onClickIcon('suffix')" size="22"></uni-icons>
</template>
<template v-else>
<uni-icons v-if="clearable && isVal && !disabled && type !== 'textarea'" class="content-clear-icon" :class="{ 'is-textarea-icon': type === 'textarea' }" type="clear" :size="clearSize" :color="msg ? '#dd524d' : focusShow ? primaryColor : '#c0c4cc'" @click="onClear"></uni-icons>
</template>
<slot name="right"></slot>
</view>
</view>
</template>
<script>
/**
* Easyinput 输入框
* @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。
* @tutorial https://ext.dcloud.net.cn/plugin?id=3455
* @property {String} value 输入内容
* @property {String } type 输入框的类型默认text password/text/textarea/..
* @value text 文本输入键盘
* @value textarea 多行文本输入键盘
* @value password 密码输入键盘
* @value number 数字输入键盘注意iOS上app-vue弹出的数字键盘并非9宫格方式
* @value idcard 身份证输入键盘信、支付宝、百度、QQ小程序
* @value digit 带小数点的数字键盘 App的nvue页面、微信、支付宝、百度、头条、QQ小程序支持
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件点击可清空输入框内容默认true
* @property {Boolean} autoHeight 是否自动增高输入区域type为textarea时有效默认true
* @property {String } placeholder 输入框的提示文字
* @property {String } placeholderStyle placeholder的样式(内联样式,字符串),如"color: #ddd"
* @property {Boolean} focus 是否自动获得焦点默认false
* @property {Boolean} disabled 是否禁用默认false
* @property {Number } maxlength 最大输入长度,设置为 -1 的时候不限制最大长度默认140
* @property {String } confirmType 设置键盘右下角按钮的文字仅在type="text"时生效默认done
* @property {Number } clearSize 清除图标的大小单位px默认15
* @property {String} prefixIcon 输入框头部图标
* @property {String} suffixIcon 输入框尾部图标
* @property {String} primaryColor 设置主题色(默认#2979ff
* @property {Boolean} trim 是否自动去除两端的空格
* @property {Boolean} cursorSpacing 指定光标与键盘的距离,单位 px
* @property {Boolean} ajust-position 当键盘弹起时是否上推内容默认值true
* @value both 去除两端空格
* @value left 去除左侧空格
* @value right 去除右侧空格
* @value start 去除左侧空格
* @value end 去除右侧空格
* @value all 去除全部空格
* @value none 不去除空格
* @property {Boolean} inputBorder 是否显示input输入框的边框默认true
* @property {Boolean} passwordIcon type=password时是否显示小眼睛图标
* @property {Object} styles 自定义颜色
* @event {Function} input 输入框内容发生变化时触发
* @event {Function} focus 输入框获得焦点时触发
* @event {Function} blur 输入框失去焦点时触发
* @event {Function} confirm 点击完成按钮时触发
* @event {Function} iconClick 点击图标时触发
* @example <uni-easyinput v-model="mobile"></uni-easyinput>
*/
function obj2strClass(obj) {
let classess = '';
for (let key in obj) {
const val = obj[key];
if (val) {
classess += `${key} `;
}
}
return classess;
}
function obj2strStyle(obj) {
let style = '';
for (let key in obj) {
const val = obj[key];
style += `${key}:${val};`;
}
return style;
}
export default {
name: 'uni-easyinput',
emits: [
'click',
'iconClick',
'update:modelValue',
'input',
'focus',
'blur',
'confirm',
'clear',
'eyes',
'change',
'keyboardheightchange'
],
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
options: {
// #ifdef MP-TOUTIAO
virtualHost: false,
// #endif
// #ifndef MP-TOUTIAO
virtualHost: true
// #endif
},
inject: {
form: {
from: 'uniForm',
default: null
},
formItem: {
from: 'uniFormItem',
default: null
}
},
props: {
name: String,
value: [Number, String],
modelValue: [Number, String],
type: {
type: String,
default: 'text'
},
clearable: {
type: Boolean,
default: true
},
autoHeight: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: ' '
},
placeholderStyle: String,
focus: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
maxlength: {
type: [Number, String],
default: 140
},
confirmType: {
type: String,
default: 'done'
},
clearSize: {
type: [Number, String],
default: 24
},
inputBorder: {
type: Boolean,
default: true
},
prefixIcon: {
type: String,
default: ''
},
suffixIcon: {
type: String,
default: ''
},
trim: {
type: [Boolean, String],
default: false
},
cursorSpacing: {
type: Number,
default: 0
},
passwordIcon: {
type: Boolean,
default: true
},
adjustPosition: {
type: Boolean,
default: true
},
primaryColor: {
type: String,
default: '#2979ff'
},
styles: {
type: Object,
default () {
return {
color: '#333',
backgroundColor: '#fff',
disableColor: '#F7F6F6',
borderColor: '#e5e5e5'
};
}
},
errorMessage: {
type: [String, Boolean],
default: ''
},
// #ifdef MP-ALIPAY
enableNative: {
type: Boolean,
default: false
}
// #endif
},
data() {
return {
focused: false,
val: '',
showMsg: '',
border: false,
isFirstBorder: false,
showClearIcon: false,
showPassword: false,
focusShow: false,
localMsg: '',
isEnter: false // 用于判断当前是否是使用回车操作
};
},
computed: {
// 输入框内是否有值
isVal() {
const val = this.val;
// fixed by mehaotian 处理值为0的情况字符串0不在处理范围
if (val || val === 0) {
return true;
}
return false;
},
msg() {
// console.log('computed', this.form, this.formItem);
// if (this.form) {
// return this.errorMessage || this.formItem.errMsg;
// }
// TODO 处理头条 formItem 中 errMsg 不更新的问题
return this.localMsg || this.errorMessage;
},
// 因为uniapp的input组件的maxlength组件必须要数值这里转为数值用户可以传入字符串数值
inputMaxlength() {
return Number(this.maxlength);
},
// 处理外层样式的style
boxStyle() {
return `color:${
this.inputBorder && this.msg ? '#e43d33' : this.styles.color
};`;
},
// input 内容的类和样式处理
inputContentClass() {
return obj2strClass({
'is-input-border': this.inputBorder,
'is-input-error-border': this.inputBorder && this.msg,
'is-textarea': this.type === 'textarea',
'is-disabled': this.disabled,
'is-focused': this.focusShow
});
},
inputContentStyle() {
const focusColor = this.focusShow ?
this.primaryColor :
this.styles.borderColor;
const borderColor =
this.inputBorder && this.msg ? '#dd524d' : focusColor;
return obj2strStyle({
'border-color': borderColor || '#e5e5e5',
'background-color': this.disabled ?
this.styles.disableColor : this.styles.backgroundColor
});
},
// input右侧样式
inputStyle() {
const paddingRight =
this.type === 'password' || this.clearable || this.prefixIcon ?
'' :
'10px';
return obj2strStyle({
'padding-right': paddingRight,
'padding-left': this.prefixIcon ? '' : '10px'
});
}
},
watch: {
value(newVal) {
// fix by mehaotian 解决 值为null的情况下input报错的bug
if (newVal === null) {
this.val = '';
return
}
this.val = newVal;
},
modelValue(newVal) {
if (newVal === null) {
this.val = '';
return
}
this.val = newVal;
},
focus(newVal) {
this.$nextTick(() => {
this.focused = this.focus;
this.focusShow = this.focus;
});
}
},
created() {
this.init();
// TODO 处理头条vue3 computed 不监听 inject 更改的问题formItem.errMsg
if (this.form && this.formItem) {
this.$watch('formItem.errMsg', newVal => {
this.localMsg = newVal;
});
}
},
mounted() {
this.$nextTick(() => {
this.focused = this.focus;
this.focusShow = this.focus;
});
},
methods: {
/**
* 初始化变量值
*/
init() {
if (this.value || this.value === 0) {
this.val = this.value;
} else if (
this.modelValue ||
this.modelValue === 0 ||
this.modelValue === ''
) {
this.val = this.modelValue;
} else {
// fix by ht 如果初始值为null则input报错待框架修复
this.val = '';
}
},
/**
* 点击图标时触发
* @param {Object} type
*/
onClickIcon(type) {
this.$emit('iconClick', type);
},
/**
* 显示隐藏内容,密码框时生效
*/
onEyes() {
this.showPassword = !this.showPassword;
this.$emit('eyes', this.showPassword);
},
/**
* 输入时触发
* @param {Object} event
*/
onInput(event) {
let value = event.detail.value;
// 判断是否去除空格
if (this.trim) {
if (typeof this.trim === 'boolean' && this.trim) {
value = this.trimStr(value);
}
if (typeof this.trim === 'string') {
value = this.trimStr(value, this.trim);
}
}
if (this.errMsg) this.errMsg = '';
this.val = value;
// TODO 兼容 vue2
this.$emit('input', value);
// TODO 兼容 vue3
this.$emit('update:modelValue', value);
},
/**
* 外部调用方法
* 获取焦点时触发
* @param {Object} event
*/
onFocus() {
this.$nextTick(() => {
this.focused = true;
});
this.$emit('focus', null);
},
_Focus(event) {
this.focusShow = true;
this.$emit('focus', event);
},
/**
* 外部调用方法
* 失去焦点时触发
* @param {Object} event
*/
onBlur() {
this.focused = false;
this.$emit('blur', null);
},
_Blur(event) {
let value = event.detail.value;
this.focusShow = false;
this.$emit('blur', event);
// 根据类型返回值在event中获取的值理论上讲都是string
if (this.isEnter === false) {
this.$emit('change', this.val);
}
// 失去焦点时参与表单校验
if (this.form && this.formItem) {
const { validateTrigger } = this.form;
if (validateTrigger === 'blur') {
this.formItem.onFieldChange();
}
}
},
/**
* 按下键盘的发送键
* @param {Object} e
*/
onConfirm(e) {
this.$emit('confirm', this.val);
this.isEnter = true;
this.$emit('change', this.val);
this.$nextTick(() => {
this.isEnter = false;
});
},
/**
* 清理内容
* @param {Object} event
*/
onClear(event) {
this.val = '';
// TODO 兼容 vue2
this.$emit('input', '');
// TODO 兼容 vue2
// TODO 兼容 vue3
this.$emit('update:modelValue', '');
// 点击叉号触发
this.$emit('clear');
},
/**
* 键盘高度发生变化的时候触发此事件
* 兼容性微信小程序2.7.0+、App 3.1.0+
* @param {Object} event
*/
onkeyboardheightchange(event) {
this.$emit('keyboardheightchange', event);
},
/**
* 去除空格
*/
trimStr(str, pos = 'both') {
if (pos === 'both') {
return str.trim();
} else if (pos === 'left') {
return str.trimLeft();
} else if (pos === 'right') {
return str.trimRight();
} else if (pos === 'start') {
return str.trimStart();
} else if (pos === 'end') {
return str.trimEnd();
} else if (pos === 'all') {
return str.replace(/\s+/g, '');
} else if (pos === 'none') {
return str;
}
return str;
}
}
};
</script>
<style lang="scss">
$uni-error: #e43d33;
$uni-border-1: #dcdfe6 !default;
.uni-easyinput {
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
flex: 1;
position: relative;
text-align: left;
color: #333;
font-size: 14px;
}
.uni-easyinput__content {
flex: 1;
/* #ifndef APP-NVUE */
width: 100%;
display: flex;
box-sizing: border-box;
// min-height: 36px;
/* #endif */
flex-direction: row;
align-items: center;
// 处理border动画刚开始显示黑色的问题
border-color: #fff;
transition-property: border-color;
transition-duration: 0.3s;
}
.uni-easyinput__content-input {
/* #ifndef APP-NVUE */
width: auto;
/* #endif */
position: relative;
overflow: hidden;
flex: 1;
line-height: 1;
font-size: 14px;
height: 35px;
// min-height: 36px;
/*ifdef H5*/
& ::-ms-reveal {
display: none;
}
& ::-ms-clear {
display: none;
}
& ::-o-clear {
display: none;
}
/*endif*/
}
.uni-easyinput__placeholder-class {
color: #999;
font-size: 12px;
// font-weight: 200;
}
.is-textarea {
align-items: flex-start;
}
.is-textarea-icon {
margin-top: 5px;
}
.uni-easyinput__content-textarea {
position: relative;
overflow: hidden;
flex: 1;
line-height: 1.5;
font-size: 14px;
margin: 6px;
margin-left: 0;
height: 80px;
min-height: 80px;
/* #ifndef APP-NVUE */
min-height: 80px;
width: auto;
/* #endif */
}
.input-padding {
padding-left: 10px;
}
.content-clear-icon {
padding: 0 5px;
}
.label-icon {
margin-right: 5px;
margin-top: -1px;
}
// 显示边框
.is-input-border {
/* #ifndef APP-NVUE */
display: flex;
box-sizing: border-box;
/* #endif */
flex-direction: row;
align-items: center;
border: 1px solid $uni-border-1;
border-radius: 4px;
/* #ifdef MP-ALIPAY */
overflow: hidden;
/* #endif */
}
.uni-error-message {
position: absolute;
bottom: -17px;
left: 0;
line-height: 12px;
color: $uni-error;
font-size: 12px;
text-align: left;
}
.uni-error-msg--boeder {
position: relative;
bottom: 0;
line-height: 22px;
}
.is-input-error-border {
border-color: $uni-error;
.uni-easyinput__placeholder-class {
color: mix(#fff, $uni-error, 50%);
}
}
.uni-easyinput--border {
margin-bottom: 0;
padding: 10px 15px;
// padding-bottom: 0;
border-top: 1px #eee solid;
}
.uni-easyinput-error {
padding-bottom: 0;
}
.is-first-border {
/* #ifndef APP-NVUE */
border: none;
/* #endif */
/* #ifdef APP-NVUE */
border-width: 0;
/* #endif */
}
.is-disabled {
background-color: #f7f6f6;
color: #d5d5d5;
.uni-easyinput__placeholder-class {
color: #d5d5d5;
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,88 @@
{
"id": "uni-easyinput",
"displayName": "uni-easyinput 增强输入框",
"version": "1.1.19",
"description": "Easyinput 组件是对原生input组件的增强",
"keywords": [
"uni-ui",
"uniui",
"input",
"uni-easyinput",
"输入框"
],
"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": "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,11 @@
### Easyinput 增强输入框
> **组件名uni-easyinput**
> 代码块: `uEasyinput`
easyinput 组件是对原生input组件的增强 ,是专门为配合表单组件[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)而设计的easyinput 内置了边框,图标等,同时包含 input 所有功能
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-easyinput)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@@ -0,0 +1,23 @@
## 2.1.12024-03-20
- 优化 app下边框过窄导致不显示的bug
## 2.1.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-tag](https://uniapp.dcloud.io/component/uniui/uni-tag)
## 2.0.02021-11-09
- 新增 提供组件设计资源,组件样式调整
- 移除 插槽
- 移除 type 属性的 royal 选项
## 1.1.12021-08-11
- type 不是 default 时size 为 small 字体大小显示不正确
## 1.1.02021-07-30
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.0.72021-06-18
- 修复 uni-tag 在字节跳动小程序上 css 类名编译错误的 bug
## 1.0.62021-06-04
- 修复 未定义 sass 变量 "$uni-color-royal" 的bug
## 1.0.52021-05-10
- 修复 royal 类型无效的bug
- 修复 uni-tag 宽度不自适应的bug
- 新增 uni-tag 支持属性 custom-style 自定义样式
## 1.0.42021-02-05
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,252 @@
<template>
<text class="uni-tag" v-if="text" :class="classes" :style="customStyle" @click="onClick">{{text}}</text>
</template>
<script>
/**
* Tag 标签
* @description 用于展示1个或多个文字标签可点击切换选中、不选中的状态
* @tutorial https://ext.dcloud.net.cn/plugin?id=35
* @property {String} text 标签内容
* @property {String} size = [default|small|mini] 大小尺寸
* @value default 正常
* @value small 小尺寸
* @value mini 迷你尺寸
* @property {String} type = [default|primary|successwarningerror] 颜色类型
* @value default 灰色
* @value primary 蓝色
* @value success 绿色
* @value warning 黄色
* @value error 红色
* @property {Boolean} disabled = [true|false] 是否为禁用状态
* @property {Boolean} inverted = [true|false] 是否无需背景颜色(空心标签)
* @property {Boolean} circle = [true|false] 是否为圆角
* @event {Function} click 点击 Tag 触发事件
*/
export default {
name: "UniTag",
emits: ['click'],
props: {
type: {
// 标签类型default、primary、success、warning、error、royal
type: String,
default: "default"
},
size: {
// 标签大小 normal, small
type: String,
default: "normal"
},
// 标签内容
text: {
type: String,
default: ""
},
disabled: {
// 是否为禁用状态
type: [Boolean, String],
default: false
},
inverted: {
// 是否为空心
type: [Boolean, String],
default: false
},
circle: {
// 是否为圆角样式
type: [Boolean, String],
default: false
},
mark: {
// 是否为标记样式
type: [Boolean, String],
default: false
},
customStyle: {
type: String,
default: ''
}
},
computed: {
classes() {
const {
type,
disabled,
inverted,
circle,
mark,
size,
isTrue
} = this
const classArr = [
'uni-tag--' + type,
'uni-tag--' + size,
isTrue(disabled) ? 'uni-tag--disabled' : '',
isTrue(inverted) ? 'uni-tag--' + type + '--inverted' : '',
isTrue(circle) ? 'uni-tag--circle' : '',
isTrue(mark) ? 'uni-tag--mark' : '',
// type === 'default' ? 'uni-tag--default' : 'uni-tag-text',
isTrue(inverted) ? 'uni-tag--inverted uni-tag-text--' + type : '',
size === 'small' ? 'uni-tag-text--small' : ''
]
// 返回类的字符串,兼容字节小程序
return classArr.join(' ')
}
},
methods: {
isTrue(value) {
return value === true || value === 'true'
},
onClick() {
if (this.isTrue(this.disabled)) return
this.$emit("click");
}
}
};
</script>
<style lang="scss" scoped>
$uni-primary: #2979ff !default;
$uni-success: #18bc37 !default;
$uni-warning: #f3a73f !default;
$uni-error: #e43d33 !default;
$uni-info: #8f939c !default;
$tag-default-pd: 4px 7px;
$tag-small-pd: 2px 5px;
$tag-mini-pd: 1px 3px;
.uni-tag {
line-height: 14px;
font-size: 12px;
font-weight: 200;
padding: $tag-default-pd;
color: #fff;
border-radius: 3px;
background-color: $uni-info;
border-width: 1px;
border-style: solid;
border-color: $uni-info;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
// size attr
&--default {
font-size: 12px;
}
&--default--inverted {
color: $uni-info;
border-color: $uni-info;
}
&--small {
padding: $tag-small-pd;
font-size: 12px;
border-radius: 2px;
}
&--mini {
padding: $tag-mini-pd;
font-size: 12px;
border-radius: 2px;
}
// type attr
&--primary {
background-color: $uni-primary;
border-color: $uni-primary;
color: #fff;
}
&--success {
color: #fff;
background-color: $uni-success;
border-color: $uni-success;
}
&--warning {
color: #fff;
background-color: $uni-warning;
border-color: $uni-warning;
}
&--error {
color: #fff;
background-color: $uni-error;
border-color: $uni-error;
}
&--primary--inverted {
color: $uni-primary;
border-color: $uni-primary;
}
&--success--inverted {
color: $uni-success;
border-color: $uni-success;
}
&--warning--inverted {
color: $uni-warning;
border-color: $uni-warning;
}
&--error--inverted {
color: $uni-error;
border-color: $uni-error;
}
&--inverted {
background-color: #fff;
}
// other attr
&--circle {
border-radius: 15px;
}
&--mark {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 15px;
border-bottom-right-radius: 15px;
}
&--disabled {
opacity: 0.5;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
}
.uni-tag-text {
color: #fff;
font-size: 14px;
&--primary {
color: $uni-primary;
}
&--success {
color: $uni-success;
}
&--warning {
color: $uni-warning;
}
&--error {
color: $uni-error;
}
&--small {
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,84 @@
{
"id": "uni-tag",
"displayName": "uni-tag 标签",
"version": "2.1.1",
"description": "Tag 组件用于展示1个或多个文字标签可点击切换选中、不选中的状态。",
"keywords": [
"uni-ui",
"uniui",
"",
"tag",
"标签"
],
"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"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
## Tag 标签
> **组件名uni-tag**
> 代码块: `uTag`
用于展示1个或多个文字标签可点击切换选中、不选中的状态 。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-tag)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

5
util/enums.js Normal file
View File

@@ -0,0 +1,5 @@
export const EExpressType = {
SF: "SF",
ZTO: "ZTO",
BEST: "Best",
};