库存盘点和项目的初版

This commit is contained in:
砂糖
2025-07-26 14:06:06 +08:00
parent 56409a9340
commit bf05e84eee
18 changed files with 3112 additions and 4 deletions

45
api/oa/oaContract.js Normal file
View File

@@ -0,0 +1,45 @@
import request from "@/util/oaRequest"
// 根据项目id查询合同信息
export function findContractByProjectId(query) {
return request({
url: '/oa/oaContract/findContract',
method: 'get',
params: query
})
}
// 根据项目id查询合同列表
export function selectContractByProjectId(query) {
return request({
url: '/oa/oaContract/selectContract',
method: 'get',
params: query
})
}
// 新增合同管理
export function addOaContract(data) {
return request({
url: '/oa/oaContract',
method: 'post',
data: data
})
}
// 修改合同管理
export function updateOaContract(data) {
return request({
url: '/oa/oaContract',
method: 'put',
data: data
})
}
// 删除合同管理
export function delOaContract(contractId) {
return request({
url: '/oa/oaContract/' + contractId,
method: 'delete'
})
}

10
api/oa/wms/stock.js Normal file
View File

@@ -0,0 +1,10 @@
import request from "@/util/oaRequest"
// 查询库存管理列表
export function listStock(query) {
return request({
url: '/oa/oaWarehouse/list',
method: 'get',
params: query
})
}

View File

@@ -317,6 +317,38 @@
"navigationBarTitleText" : "快递信息",
"navigationStyle": "default"
}
},
{
"path" : "pages/workbench/project/project",
"style" :
{
"navigationBarTitleText" : "项目中心",
"navigationStyle": "default"
}
},
{
"path" : "pages/workbench/wms/wms",
"style" :
{
"navigationBarTitleText" : "库存管理",
"navigationStyle": "default"
}
},
{
"path" : "pages/workbench/project/add",
"style" :
{
"navigationBarTitleText" : "新建项目",
"navigationStyle": "default"
}
},
{
"path" : "pages/workbench/project/detail",
"style" :
{
"navigationBarTitleText" : "项目详情",
"navigationStyle": "default"
}
}
],
"tabBar": {

View File

@@ -24,9 +24,18 @@
</view>
<view class="entry-item" @click="goExpress">
<image class="entry-icon" src="/static/images/express.svg" mode="aspectFit"></image>
<text class="entry-text">快递信息</text>
<text class="entry-text">快递信息</text>
</view>
<view class="entry-item" @click="goProject">
<image class="entry-icon" src="/static/images/project.png" mode="aspectFit"></image>
<text class="entry-text">项目中心</text>
</view>
<view class="entry-item" @click="goStock">
<image class="entry-icon" src="/static/images/stock.png" mode="aspectFit"></image>
<text class="entry-text">库存盘点</text>
</view>
</view>
</view>
</template>
@@ -81,7 +90,17 @@ export default {
uni.navigateTo({
url: '/pages/workbench/express/express'
})
}
},
goProject() {
uni.navigateTo({
url: '/pages/workbench/project/project'
})
},
goStock() {
uni.navigateTo({
url: '/pages/workbench/wms/wms'
})
}
},
};
</script>

View File

@@ -0,0 +1,139 @@
<template>
<view class="add-project-container">
<uni-forms ref="form" :model="form" label-width="100" class="form-box">
<uni-forms-item label="项目名称" name="projectName" required>
<uni-easyinput v-model="form.projectName" placeholder="请输入项目名称" />
</uni-forms-item>
<uni-forms-item label="项目编号" name="projectNum" required>
<uni-easyinput v-model="form.projectNum" placeholder="请输入项目编号" />
</uni-forms-item>
<uni-forms-item label="项目类型" name="projectType" required>
<oa-dict-select v-model="form.projectType" dict-type="sys_project_type" placeholder="请选择项目类型" />
</uni-forms-item>
<uni-forms-item label="贸易类型" name="tradeType" required>
<uni-data-select v-model="form.tradeType" :localdata="tradeTypeList" placeholder="请选择贸易类型" />
</uni-forms-item>
<uni-forms-item label="代号类型" name="projectCode" required>
<oa-dict-select v-model="form.projectCode" dict-type="sys_project_code" placeholder="请选择代号类型" />
</uni-forms-item>
<uni-forms-item label="优先级" name="projectGrade" required>
<oa-dict-select v-model="form.projectGrade" dict-type="sys_sort_grade" placeholder="请选择优先级" />
</uni-forms-item>
<uni-forms-item label="负责人" name="functionary" required>
<uni-easyinput v-model="form.functionary" placeholder="请输入负责人" />
</uni-forms-item>
<uni-forms-item label="项目地址" name="address">
<uni-easyinput v-model="form.address" placeholder="请输入项目地址" />
</uni-forms-item>
<uni-forms-item label="项目总金额" name="funds" required>
<uni-easyinput v-model="form.funds" placeholder="请输入项目总金额" type="number" />
</uni-forms-item>
<uni-forms-item label="交货期" name="delivery" required>
<uni-easyinput v-model="form.delivery" placeholder="请输入交货期" />
</uni-forms-item>
<uni-forms-item label="项目周期" name="period" required>
<uni-datetime-picker v-model="form.period" type="daterange" rangeSeparator="至" start-placeholder="开始日期"
end-placeholder="结束日期" />
</uni-forms-item>
<uni-forms-item label="质保期" name="guarantee">
<uni-easyinput v-model="form.guarantee" placeholder="请输入质保期" />
</uni-forms-item>
<uni-forms-item label="预付款" name="prePay">
<uni-easyinput v-model="form.prePay" placeholder="请输入预付款" type="number" />
</uni-forms-item>
<uni-forms-item label="项目介绍" name="introduction">
<uni-easyinput v-model="form.introduction" type="textarea" placeholder="请输入项目介绍" />
</uni-forms-item>
<uni-forms-item label="备注" name="remark">
<uni-easyinput v-model="form.remark" type="textarea" placeholder="请输入备注" />
</uni-forms-item>
<!-- <uni-forms-item label="附件" name="accessory">
<uni-file-picker v-model="form.accessory" file-extname=".jpg,.png,.pdf,.doc,.docx" :limit="5" />
</uni-forms-item> -->
<view class="form-actions">
<button type="primary" @click="submitForm">保存</button>
</view>
</uni-forms>
</view>
</template>
<script>
import { addProject } from '@/api/oa/project';
export default {
data() {
return {
form: {
projectName: '',
projectNum: '',
projectType: '',
tradeType: '',
projectCode: '',
projectGrade: '',
functionary: '',
address: '',
funds: '',
delivery: '',
period: [],
guarantee: '',
prePay: '',
introduction: '',
remark: '',
accessory: []
},
tradeTypeList: [
{ value: 0, text: '内贸' },
{ value: 1, text: '外贸' }
// TODO: 替换为实际字典数据
],
projectGradeList: [
{ value: 'high', text: '高' },
{ value: 'middle', text: '中' },
{ value: 'low', text: '低' }
]
}
},
methods: {
submitForm() {
// 校验并提交表单
this.$refs.form.validate().then(res => {
if (res) {
// TODO: 调用新增项目API
console.log(res);
const payload = {
...res,
beginTime: res.period[0] + ' 00:00:00',
finishTime: res.period[1] + ' 00:00:00',
accessory: ''
}
addProject(payload).then(_ => {
uni.showToast({ title: '保存成功', icon: 'success' });
uni.navigateBack()
})
}
}).catch(() => {
uni.showToast({ title: '请完善信息', icon: 'none' });
});
}
}
}
</script>
<style scoped>
.add-project-container {
padding: 24rpx;
}
.form-box {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 12rpx #eee;
}
.form-actions {
margin-top: 32rpx;
text-align: center;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<view class="detail-page">
<view class="detail-header">
<text class="header-title">{{ projectDetail.projectName }}</text>
<text class="header-subtitle">{{ projectDetail.projectNum }}</text>
</view>
<view class="detail-content">
<view style="display: flex; justify-content: flex-start; gap: 10rpx">
<uni-tag v-if="projectDetail.projectStatus == 1" size="normal" type="success" text="进度完成"></uni-tag>
<uni-tag v-else-if="projectDetail.projectStatus == 0" size="normal" type="warning" text="进行中"></uni-tag>
<uni-tag v-if="projectDetail.tradeType == 0" size="normal" type="warning" text="内贸"></uni-tag>
<uni-tag v-else-if="projectDetail.tradeType == 1" size="normal" type="success" text="外贸"></uni-tag>
</view>
<view class="info-item">
<text class="info-label">项目类型</text>
<text class="info-value">{{ projectDetail.projectType || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">项目地址</text>
<text class="info-value">{{ projectDetail.address || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">项目总金额</text>
<text class="info-value">{{ projectDetail.funds || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">负责人</text>
<text class="info-value">{{ projectDetail.functionary || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">项目周期</text>
<text class="info-value">{{ projectDetail.beginTime || '-' }} {{ projectDetail.finishTime || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">交付时间</text>
<text class="info-value">{{ projectDetail.delivery || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">保证期</text>
<text class="info-value">{{ projectDetail.guarantee || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">备注</text>
<text class="info-value">{{ projectDetail.remark || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">延期原因</text>
<text class="info-value">{{ projectDetail.postponeReason || '无' }}</text>
</view>
<view class="info-item" v-if="projectDetail.prePay > 0">
<text class="info-label">预付款</text>
<text class="info-value">{{ projectDetail.prePay }}</text>
</view>
</view>
<!-- 合同管理 -->
<view class="contract-management">
<text class="contract-title">合同管理</text>
<view v-if="contractDetail && contractDetail.length > 0">
<view class="info-item">
<text class="info-label">合同类型</text>
<text class="info-value">{{ getContractType(contractDetail.contractType) }}</text>
</view>
<view class="info-item">
<text class="info-label">合同编号</text>
<text class="info-value">{{ contractDetail.contractNum || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">合同金额</text>
<text class="info-value">{{ contractDetail.amount || '-' }}</text>
</view>
</view>
<view v-else>
暂无合同
</view>
</view>
<view v-if="projectDetail.projectStatus == 0" style="margin-top: 20px; text-align: center;">
<button @click="handleClosure">结项</button>
</view>
</view>
</template>
<script>
import { getProject, updateProject } from '@/api/oa/project.js'; // 确保导入正确的 API 方法
import { selectContractByProjectId } from '@/api/oa/oaContract.js';
export default {
data() {
return {
projectDetail: {}, // 存储项目详情
contractDetail: null, // 存储合同详情
};
},
onLoad(options) {
this.fetchProjectDetail(options.id); // 获取项目 ID
},
methods: {
fetchProjectDetail(projectId) {
getProject(projectId).then(res => {
if (res.code === 200) {
this.projectDetail = res.data; // 将返回的数据存储到 projectDetail 中
this.fetchContractDetail(projectId); // 获取合同信息
} else {
console.error('获取项目详情失败:', res.msg);
}
}).catch(error => {
console.error('获取项目详情失败:', error);
});
},
handleClosure() {
// uniapp二次确认
uni.showModal({
title: '提示',
content: '确定要结项吗?',
success: (res) => {
if (res.confirm) {
updateProject({...this.projectDetail, projectStatus: 1}).then(res => {
this.fetchProjectDetail(this.projectDetail.projectId);
});
}
}
});
},
fetchContractDetail(projectId) {
selectContractByProjectId({ projectId }).then(res => {
if (res.code === 200) {
this.contractDetail = res.data; // 将返回的合同信息存储到 contractDetail 中
} else {
console.error('获取合同信息失败:', res.msg);
}
}).catch(error => {
console.error('获取合同信息失败:', error);
});
},
getContractType(contractType) {
const contractTypes = {
1: '采购合同',
2: '项目合同',
};
return contractTypes[contractType] || '未知类型';
},
},
};
</script>
<style scoped>
.detail-page {
padding: 20px; /* 添加内边距 */
}
.detail-header {
margin-bottom: 15px; /* 下边距 */
}
.header-title {
font-size: 24px; /* 标题字号 */
font-weight: bold; /* 加粗 */
}
.header-subtitle {
font-size: 18px; /* 副标题字号 */
color: #666; /* 较淡的字体颜色 */
}
.detail-content {
border: 1px solid #ccc; /* 边框 */
border-radius: 10px; /* 圆角 */
padding: 15px; /* 内边距 */
background-color: #f9f9f9; /* 背景色 */
}
.info-item {
display: flex; /* 使用 Flexbox 布局 */
justify-content: space-between; /* 在一行内均匀分布 */
margin-top: 10px; /* 上边距 */
}
.info-label {
color: #333; /* 标签颜色 */
font-weight: bold; /* 标签加粗 */
}
.info-value {
color: #666; /* 值的颜色 */
}
.contract-management {
margin-top: 20px; /* 上边距 */
}
.contract-title {
font-size: 20px; /* 合同标题字号 */
font-weight: bold; /* 加粗 */
margin-bottom: 10px; /* 下边距 */
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<view class="project-page">
<!-- 新增按钮 -->
<view class="add-btn-row">
<view class="button-group">
<uni-icons
type="gift"
size="30"
@click="toggleQualityFilter"
:color="qualityFilter ? '#ffcc00' : '#333'"
/>
<!-- <uni-icons
type="gear"
size="30"
@click="openSettingsPopup"
color="#333"
/> -->
<uni-icons
type="plus"
size="30"
@click="handleAdd"
color="#333"
/>
</view>
</view>
<!-- 项目列表 -->
<scroll-view scroll-y @scrolltolower="loadMore" :style="{ height: scrollHeight + 'px' }">
<view class="project-grid">
<view
class="project-card"
v-for="item in projectList"
:key="item.projectId"
@click="handleDetail(item)"
>
<view class="project-title">{{ item.projectName }}</view>
<view class="project-info">
<text>负责人{{ item.functionary }}</text>
<text :style="getRemainTimeStyle(item.remainTime)">
剩余时间{{ item.remainTime }}
</text>
<text>项目总金额{{ item.funds }}</text>
<text v-if="item.prePay > 0" style="color: #ffcc00;">预付款{{ item.prePay }}</text>
<view style="display: flex; justify-content: flex-start; gap: 10rpx; position: absolute; bottom: 20rpx; margin-top: 10rpx;">
<uni-tag v-if="item.projectStatus == 1" size="mini" type="success" text="进度完成"></uni-tag>
<uni-tag v-else-if="item.projectStatus == 0" size="mini" type="warning" text="进行中"></uni-tag>
<uni-tag v-if="item.tradeType == 0" size="mini" type="warning" text="内贸"></uni-tag>
<uni-tag v-else-if="item.tradeType == 1" size="mini" type="success" text="外贸"></uni-tag>
</view>
</view>
</view>
</view>
<!-- 加载更多组件 -->
<uni-load-more :status="loadMoreStatus"></uni-load-more>
</scroll-view>
</view>
</template>
<script>
import { listProject, addProject, updateProject, delProject, getProject } from '@/api/oa/project.js'
export default {
data() {
return {
projectList: [],
form: {},
editType: '', // add/edit
page: 1, // 当前页码
pageSize: 10, // 每页项目数量
selectedProject: {}, // 选中的项目
loadMoreStatus: 'more', // 加载更多状态
scrollHeight: 0, // 滚动区域高度
qualityFilter: false, // 优质筛选开关
}
},
onShow() {
this.getList();
this.calculateScrollHeight();
},
methods: {
calculateScrollHeight() {
const systemInfo = uni.getSystemInfoSync();
const tabHeight = 75; // tab高度
const containerPadding = 40; // 容器padding
this.scrollHeight = systemInfo.windowHeight - tabHeight - containerPadding + 80;
},
getList() {
const params = { pageNum: this.page, pageSize: this.pageSize };
if (this.qualityFilter) {
params.prePay = 0.1; // 添加优质筛选参数
}
listProject(params).then(res => {
const newProjects = (res.rows || []).map(row => ({
...row,
projectTypeLabel: row.projectType,
projectStatusLabel: row.projectStatus,
}));
this.projectList = [...this.projectList, ...newProjects];
if (newProjects.length < this.pageSize) {
this.loadMoreStatus = 'noMore'; // 没有更多数据
} else {
this.loadMoreStatus = 'more'; // 还有更多数据
}
});
},
loadMore() {
if (this.loadMoreStatus === 'more') {
console.log('加载更多');
this.page++;
this.getList();
}
},
handleAdd() {
uni.navigateTo({
url: '/pages/workbench/project/add'
})
},
handleDetail(item) {
uni.navigateTo({
url: '/pages/workbench/project/detail?id=' + item.projectId
})
},
toggleQualityFilter() {
this.qualityFilter = !this.qualityFilter; // 切换优质筛选状态
this.page = 1; // 重置页码
this.projectList = []; // 清空当前项目列表
this.getList(); // 重新获取项目列表
},
openEditPopup(item) {
this.editType = 'edit';
getProject(item.projectId).then(res => {
this.form = { ...res.data };
this.$refs.editPopup.open();
});
},
closePopup() {
this.$refs.editPopup.close();
},
submitForm() {
if (this.editType === 'add') {
addProject(this.form).then(() => {
this.getList();
this.closePopup();
});
} else {
updateProject(this.form).then(() => {
this.getList();
this.closePopup();
});
}
},
handleClosure(item) {
const update = { ...item, projectStatus: '1' };
updateProject(update).then(() => {
this.getList();
});
},
handleDelete(item) {
delProject(item.projectId).then(() => {
this.getList();
});
},
openDetailPopup(item) {
this.selectedProject = item;
this.$refs.detailPopup.open();
},
closeDetailPopup() {
this.$refs.detailPopup.close();
},
getRemainTimeStyle(remainTime) {
if (remainTime > 10) {
return { color: '#3c763d' }; // 绿色
} else if (remainTime > 5) {
return { color: '#8a6d3b' }; // 黄色
} else {
return { color: '#a94442' }; // 红色
}
}
}
}
</script>
<style scoped>
.add-btn-row {
display: flex;
justify-content: flex-end; /* 右对齐 */
align-items: center;
margin: 20rpx;
}
.button-group {
display: flex;
gap: 10px; /* 按钮之间的间距 */
}
.popup-content {
background-color: #fff; /* 设置纯白背景 */
padding: 20px; /* 添加内边距 */
border-radius: 8px; /* 圆角 */
}
.close-button {
display: flex;
justify-content: flex-end; /* 右对齐 */
margin-bottom: 10px; /* 底部间距 */
}
.project-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.project-card {
border: 1px solid #ccc; /* 边框 */
border-radius: 10px; /* 圆角 */
background-color: #fff; /* 背景色 */
margin: 10px;
box-sizing: border-box;
width: calc(50% - 20px); /* 每行两个卡片 */
padding: 20px 15px; /* 添加内边距 */
position: relative;
}
.project-title {
font-size: 20px; /* 项目名称字号 */
font-weight: bold; /* 加粗 */
color: #333; /* 主字体颜色 */
margin-bottom: 5px; /* 下边距 */
}
.project-info text {
display: block; /* 每个信息占一行 */
margin-top: 5px; /* 上边距 */
color: #666; /* 较淡的字体颜色 */
font-size: 16px; /* 信息字号 */
line-height: 1.5; /* 行间距 */
}
.project-info {
margin-top: 10px; /* 项目信息与标题之间的间距 */
}
/* 新增的样式 */
.horizontal-actions {
display: flex;
justify-content: space-between; /* 在一行内均匀分布 */
margin-top: 10px; /* 添加顶部间距 */
}
.horizontal-actions button {
flex: 1; /* 按钮均分宽度 */
margin: 0 5px; /* 按钮之间的间距 */
}
</style>

385
pages/workbench/wms/wms.vue Normal file
View File

@@ -0,0 +1,385 @@
<template>
<view class="container">
<!-- 筛选表单 一行展示 -->
<view class="filter-form-row">
<!-- 型号筛选 -->
<view :class="['filter-item', activeFilter === 'model' ? 'filter-item-expand' : 'filter-item-collapse']">
<template v-if="activeFilter === 'model'">
<view class="input-clear-wrap">
<input
class="filter-input"
v-model="queryParams.model"
placeholder="型号"
@blur="onFilterBlur('model')"
:style="{ color: queryParams.model ? '#2979ff' : '#999' }"
/>
<view v-if="queryParams.model" class="clear-icon" @click="clearFilter('model')">
<uni-icons type="closeempty" color="#bbb" size="18" />
</view>
</view>
</template>
<template v-else>
<view class="filter-icon" @click="toggleFilter('model')">
<uni-icons type="cart" :color="queryParams.model ? '#2979ff' : '#bbb'" size="24" />
</view>
</template>
</view>
<!-- 物料名称筛选 -->
<view :class="['filter-item', activeFilter === 'name' ? 'filter-item-expand' : 'filter-item-collapse']">
<template v-if="activeFilter === 'name'">
<view class="input-clear-wrap">
<input
class="filter-input"
v-model="queryParams.name"
placeholder="物料名称"
@blur="onFilterBlur('name')"
:style="{ color: queryParams.name ? '#2979ff' : '#999' }"
/>
<view v-if="queryParams.name" class="clear-icon" @click="clearFilter('name')">
<uni-icons type="closeempty" color="#bbb" size="18" />
</view>
</view>
</template>
<template v-else>
<view class="filter-icon" @click="toggleFilter('name')">
<uni-icons type="search" :color="queryParams.name ? '#2979ff' : '#bbb'" size="24" />
</view>
</template>
</view>
<!-- 品牌筛选 -->
<view :class="['filter-item', activeFilter === 'brand' ? 'filter-item-expand' : 'filter-item-collapse']">
<template v-if="activeFilter === 'brand'">
<view class="input-clear-wrap">
<input
class="filter-input"
v-model="queryParams.brand"
placeholder="品牌"
@blur="onFilterBlur('brand')"
:style="{ color: queryParams.brand ? '#2979ff' : '#999' }"
/>
<view v-if="queryParams.brand" class="clear-icon" @click="clearFilter('brand')">
<uni-icons type="closeempty" color="#bbb" size="18" />
</view>
</view>
</template>
<template v-else>
<view class="filter-icon" @click="toggleFilter('brand')">
<uni-icons type="circle" :color="queryParams.brand ? '#2979ff' : '#bbb'" size="24" />
</view>
</template>
</view>
<button class="filter-btn" @click="resetQuery">重置</button>
</view>
<!-- 列表展示下拉加载更多 -->
<scroll-view class="list" scroll-y @scrolltolower="loadMore">
<view v-for="(item, index) in oaWarehouseList" :key="item.id" class="list-item">
<view class="row">
<text class="index">{{ index + 1 }}</text>
<text class="name">{{ item.name }}</text>
<text class="model">{{ item.model }}</text>
<text class="brand">{{ item.brand }}</text>
</view>
<view class="row">
<text class="price">单价{{ item.price }}</text>
<text :class="['inventory', item.inventory < item.threshold ? 'low' : '']">
库存{{ item.inventory }}
<text v-if="item.taskInventory !== null">({{ item.taskInventory }})</text>
</text>
<text class="unit">{{ item.unit }}</text>
<text class="specifications">{{ item.specifications }}</text>
</view>
</view>
<view v-if="oaWarehouseList.length === 0" class="empty">暂无数据</view>
<view v-if="loadingMore" class="loading-more">加载中...</view>
<view v-if="!hasMore && oaWarehouseList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</template>
<script>
import { listStock } from '@/api/oa/wms/stock.js'
import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
import uniEasyinput from '@/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue'
export default {
components: {
uniIcons
},
data() {
return {
oaWarehouseList: [],
queryParams: {
pageNum: 1,
pageSize: 20,
model: '',
name: '',
brand: ''
},
total: 0,
loadingMore: false,
hasMore: true,
activeFilter: 'model' // 当前展开的筛选项
}
},
onLoad() {
this.getList()
},
methods: {
getList(isLoadMore = false) {
if (!isLoadMore) {
this.queryParams.pageNum = 1
this.oaWarehouseList = []
this.hasMore = true
}
listStock(this.queryParams).then(res => {
const rows = res.rows || []
this.total = res.total || 0
if (isLoadMore) {
this.oaWarehouseList = this.oaWarehouseList.concat(rows)
} else {
this.oaWarehouseList = rows
}
// 判断是否还有更多
this.hasMore = this.oaWarehouseList.length < this.total
this.loadingMore = false
})
},
handleQuery() {
this.getList(false)
},
toggleFilter(type) {
this.activeFilter = type
},
onFilterBlur(type) {
// 输入框失焦后自动筛选
this.handleQuery()
},
clearFilter(type) {
this.queryParams[type] = ''
this.handleQuery()
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
model: '',
name: '',
brand: ''
}
this.getList(false)
},
loadMore() {
if (!this.hasMore || this.loadingMore) return
this.loadingMore = true
this.queryParams.pageNum++
this.getList(true)
}
}
}
</script>
<style scoped>
.input-clear-wrap {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
.clear-icon {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
z-index: 2;
background: #fff;
border-radius: 50%;
padding: 4rpx;
display: flex;
align-items: center;
justify-content: center;
}
.container {
padding: 20rpx;
}
.filter-form-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
background: #fff;
border-radius: 8rpx;
box-shadow: 0 2rpx 8rpx #eee;
padding: 8rpx 16rpx;
gap: 12rpx;
}
.filter-item {
display: flex;
align-items: center;
justify-content: center;
height: 56rpx;
transition: all 0.2s;
}
.filter-item-expand {
flex: 1 1 0%;
margin-right: 16rpx;
min-width: 120rpx;
}
.filter-item-collapse {
flex: 0 0 48rpx;
margin-right: 8rpx;
min-width: 48rpx;
max-width: 48rpx;
}
.filter-input {
width: 100%;
padding: 8rpx 12rpx;
border: 1rpx solid #eee;
border-radius: 6rpx;
background: #f8f8f8;
font-size: 28rpx;
transition: color 0.2s;
}
.filter-input {
width: 100%;
padding: 8rpx 12rpx;
border: 1rpx solid #eee;
border-radius: 6rpx;
background: #f8f8f8;
font-size: 28rpx;
transition: color 0.2s;
}
.filter-btn {
margin-right: 12rpx;
padding: 8rpx 24rpx;
background: #2979ff;
color: #fff;
border-radius: 6rpx;
font-size: 28rpx;
border: none;
}
.list {
background: #fff;
border-radius: 8rpx;
box-shadow: 0 2rpx 8rpx #eee;
height: 90vh;
overflow: hidden;
}
.list-item {
margin: 16rpx 16rpx 0 16rpx;
padding: 24rpx 20rpx 16rpx 20rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx #e5e5e5;
display: flex;
flex-direction: column;
position: relative;
}
.row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
margin-bottom: 8rpx;
}
.index {
width: 48rpx;
color: #bbb;
font-size: 28rpx;
font-weight: bold;
margin-right: 16rpx;
}
.name {
font-size: 32rpx;
font-weight: 600;
color: #2979ff;
margin-right: 24rpx;
max-width: 220rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.model {
font-size: 28rpx;
color: #333;
margin-right: 20rpx;
max-width: 160rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand {
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.price {
font-size: 28rpx;
color: #faad14;
margin-right: 20rpx;
}
.inventory {
font-size: 28rpx;
font-weight: 600;
margin-right: 20rpx;
color: #52c41a;
}
.inventory.low {
color: #ff4d4f !important;
background: #fff2f0;
border-radius: 6rpx;
padding: 0 8rpx;
}
.unit {
font-size: 26rpx;
color: #999;
margin-right: 16rpx;
}
.specifications {
font-size: 26rpx;
color: #999;
margin-right: 16rpx;
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty {
text-align: center;
color: #999;
padding: 40rpx 0;
}
.loading-more {
text-align: center;
color: #2979ff;
padding: 20rpx 0;
}
.no-more {
text-align: center;
color: #bbb;
padding: 20rpx 0;
}
</style>

BIN
static/images/project.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
static/images/stock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,100 @@
## 1.4.132024-10-08
- 修复 校验规则在抖音开发者工具上不生效的bug详见[https://ask.dcloud.net.cn/question/191933](https://ask.dcloud.net.cn/question/191933)
## 1.4.12 (2024-9-21)
- 修复 form上次修改的问题
## 1.4.11 (2024-9-14)
- 修复 binddata的兼容性问题
## 1.4.102023-11-03
- 优化 labelWidth 描述错误
## 1.4.92023-02-10
- 修复 required 参数无法动态绑定
## 1.4.82022-08-23
- 优化 根据 rules 自动添加 required 的问题
## 1.4.72022-08-22
- 修复 item 未设置 require 属性rules 设置 require 后,星号也显示的 bug详见[https://ask.dcloud.net.cn/question/151540](https://ask.dcloud.net.cn/question/151540)
## 1.4.62022-07-13
- 修复 model 需要校验的值没有声明对应字段时导致第一次不触发校验的bug
## 1.4.52022-07-05
- 新增 更多表单示例
- 优化 子表单组件过期提示的问题
- 优化 子表单组件uni-datetime-picker、uni-data-select、uni-data-picker的显示样式
## 1.4.42022-07-04
- 更新 删除组件日志
## 1.4.32022-07-04
- 修复 由 1.4.0 引发的 label 插槽不生效的bug
## 1.4.22022-07-04
- 修复 子组件找不到 setValue 报错的bug
## 1.4.12022-07-04
- 修复 uni-data-picker 在 uni-forms-item 中报错的bug
- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug
## 1.4.02022-06-30
- 【重要】组件逻辑重构,部分用法用旧版本不兼容,请注意兼容问题
- 【重要】组件使用 Provide/Inject 方式注入依赖,提供了自定义表单组件调用 uni-forms 校验表单的能力
- 新增 model 属性,等同于原 value/modelValue 属性,旧属性即将废弃
- 新增 validateTrigger 属性的 blur 值,仅 uni-easyinput 生效
- 新增 onFieldChange 方法可以对子表单进行校验可替代binddata方法
- 新增 子表单的 setRules 方法,配合自定义校验函数使用
- 新增 uni-forms-item 的 setRules 方法,配置动态表单使用可动态更新校验规则
- 优化 动态表单校验方式废弃拼接name的方式
## 1.3.32022-06-22
- 修复 表单校验顺序无序问题
## 1.3.22021-12-09
-
## 1.3.12021-11-19
- 修复 label 插槽不生效的bug
## 1.3.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-forms](https://uniapp.dcloud.io/component/uniui/uni-forms)
## 1.2.72021-08-13
- 修复 没有添加校验规则的字段依然报错的Bug
## 1.2.62021-08-11
- 修复 重置表单错误信息无法清除的问题
## 1.2.52021-08-11
- 优化 组件文档
## 1.2.42021-08-11
- 修复 表单验证只生效一次的问题
## 1.2.32021-07-30
- 优化 vue3下事件警告的问题
## 1.2.22021-07-26
- 修复 vue2 下条件编译导致destroyed生命周期失效的Bug
- 修复 1.2.1 引起的示例在小程序平台报错的Bug
## 1.2.12021-07-22
- 修复 动态校验表单默认值为空的情况下校验失效的Bug
- 修复 不指定name属性时运行报错的Bug
- 优化 label默认宽度从65调整至70使required为true且四字时不换行
- 优化 组件示例,新增动态校验示例代码
- 优化 组件文档,使用方式更清晰
## 1.2.02021-07-13
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.22021-06-25
- 修复 pattern 属性在微信小程序平台无效的问题
## 1.1.12021-06-22
- 修复 validate-trigger属性为submit且err-show-type属性为toast时不能弹出的Bug
## 1.1.02021-06-22
- 修复 只写setRules方法而导致校验不生效的Bug
- 修复 由上个办法引发的错误提示文字错位的Bug
## 1.0.482021-06-21
- 修复 不设置 label 属性 无法设置label插槽的问题
## 1.0.472021-06-21
- 修复 不设置label属性label-width属性不生效的bug
- 修复 setRules 方法与rules属性冲突的问题
## 1.0.462021-06-04
- 修复 动态删减数据导致报错的问题
## 1.0.452021-06-04
- 新增 modelValue 属性 value 即将废弃
## 1.0.442021-06-02
- 新增 uni-forms-item 可以设置单独的 rules
- 新增 validate 事件增加 keepitem 参数,可以选择那些字段不过滤
- 优化 submit 事件重命名为 validate
## 1.0.432021-05-12
- 新增 组件示例地址
## 1.0.422021-04-30
- 修复 自定义检验器失效的问题
## 1.0.412021-03-05
- 更新 校验器
- 修复 表单规则设置类型为 number 的情况下值为0校验失败的Bug
## 1.0.402021-03-04
- 修复 动态显示uni-forms-item的情况下submit 方法获取值错误的Bug
## 1.0.392021-02-05
- 调整为uni_modules目录规范
- 修复 校验器传入 int 等类型 返回String类型的Bug

View File

@@ -0,0 +1,632 @@
<template>
<view class="uni-forms-item"
:class="['is-direction-' + localLabelPos ,border?'uni-forms-item--border':'' ,border && isFirstBorder?'is-first-border':'']">
<slot name="label">
<view class="uni-forms-item__label" :class="{'no-label':!label && !required}"
:style="{width:localLabelWidth,justifyContent: localLabelAlign}">
<text v-if="required" class="is-required">*</text>
<text>{{label}}</text>
</view>
</slot>
<!-- #ifndef APP-NVUE -->
<view class="uni-forms-item__content">
<slot></slot>
<view class="uni-forms-item__error" :class="{'msg--active':msg}">
<text>{{msg}}</text>
</view>
</view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view class="uni-forms-item__nuve-content">
<view class="uni-forms-item__content">
<slot></slot>
</view>
<view class="uni-forms-item__error" :class="{'msg--active':msg}">
<text class="error-text">{{msg}}</text>
</view>
</view>
<!-- #endif -->
</view>
</template>
<script>
/**
* uni-fomrs-item 表单子组件
* @description uni-fomrs-item 表单子组件,提供了基础布局已经校验能力
* @tutorial https://ext.dcloud.net.cn/plugin?id=2773
* @property {Boolean} required 是否必填,左边显示红色"*"号
* @property {String } label 输入框左边的文字提示
* @property {Number } labelWidth label的宽度单位px默认70
* @property {String } labelAlign = [left|center|right] label的文字对齐方式默认left
* @value left label 左侧显示
* @value center label 居中
* @value right label 右侧对齐
* @property {String } errorMessage 显示的错误提示内容如果为空字符串或者false则不显示错误信息
* @property {String } name 表单域的属性名,在使用校验规则时必填
* @property {String } leftIcon 【1.4.0废弃】label左边的图标限 uni-ui 的图标名称
* @property {String } iconColor 【1.4.0废弃】左边通过icon配置的图标的颜色默认#606266
* @property {String} validateTrigger = [bind|submit|blur] 【1.4.0废弃】校验触发器方式 默认 submit
* @value bind 发生变化时触发
* @value submit 提交时触发
* @value blur 失去焦点触发
* @property {String } labelPosition = [top|left] 【1.4.0废弃】label的文字的位置默认left
* @value top 顶部显示 label
* @value left 左侧显示 label
*/
export default {
name: 'uniFormsItem',
options: {
// #ifdef MP-TOUTIAO
virtualHost: false,
// #endif
// #ifndef MP-TOUTIAO
virtualHost: true
// #endif
},
provide() {
return {
uniFormItem: this
}
},
inject: {
form: {
from: 'uniForm',
default: null
},
},
props: {
// 表单校验规则
rules: {
type: Array,
default () {
return null;
}
},
// 表单域的属性名,在使用校验规则时必填
name: {
type: [String, Array],
default: ''
},
required: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
// label的宽度
labelWidth: {
type: [String, Number],
default: ''
},
// label 居中方式,默认 left 取值 left/center/right
labelAlign: {
type: String,
default: ''
},
// 强制显示错误信息
errorMessage: {
type: [String, Boolean],
default: ''
},
// 1.4.0 弃用,统一使用 form 的校验时机
// validateTrigger: {
// type: String,
// default: ''
// },
// 1.4.0 弃用,统一使用 form 的label 位置
// labelPosition: {
// type: String,
// default: ''
// },
// 1.4.0 以下属性已经废弃,请使用 #label 插槽代替
leftIcon: String,
iconColor: {
type: String,
default: '#606266'
},
},
data() {
return {
errMsg: '',
userRules: null,
localLabelAlign: 'left',
localLabelWidth: '70px',
localLabelPos: 'left',
border: false,
isFirstBorder: false,
};
},
computed: {
// 处理错误信息
msg() {
return this.errorMessage || this.errMsg;
}
},
watch: {
// 规则发生变化通知子组件更新
'form.formRules'(val) {
// TODO 处理头条vue3 watch不生效的问题
// #ifndef MP-TOUTIAO
this.init()
// #endif
},
'form.labelWidth'(val) {
// 宽度
this.localLabelWidth = this._labelWidthUnit(val)
},
'form.labelPosition'(val) {
// 标签位置
this.localLabelPos = this._labelPosition()
},
'form.labelAlign'(val) {
}
},
created() {
this.init(true)
if (this.name && this.form) {
// TODO 处理头条vue3 watch不生效的问题
// #ifdef MP-TOUTIAO
this.$watch('form.formRules', () => {
this.init()
})
// #endif
// 监听变化
this.$watch(
() => {
const val = this.form._getDataValue(this.name, this.form.localData)
return val
},
(value, oldVal) => {
const isEqual = this.form._isEqual(value, oldVal)
// 简单判断前后值的变化,只有发生变化才会发生校验
// TODO 如果 oldVal = undefined ,那么大概率是源数据里没有值导致 ,这个情况不哦校验 ,可能不严谨 ,需要在做观察
// fix by mehaotian 暂时取消 && oldVal !== undefined 如果formData 中不存在,可能会不校验
if (!isEqual) {
const val = this.itemSetValue(value)
this.onFieldChange(val, false)
}
}, {
immediate: false
}
);
}
},
// #ifndef VUE3
destroyed() {
if (this.__isUnmounted) return
this.unInit()
},
// #endif
// #ifdef VUE3
unmounted() {
this.__isUnmounted = true
this.unInit()
},
// #endif
methods: {
/**
* 外部调用方法
* 设置规则 ,主要用于小程序自定义检验规则
* @param {Array} rules 规则源数据
*/
setRules(rules = null) {
this.userRules = rules
this.init(false)
},
// 兼容老版本表单组件
setValue() {
// console.log('setValue 方法已经弃用,请使用最新版本的 uni-forms 表单组件以及其他关联组件。');
},
/**
* 外部调用方法
* 校验数据
* @param {any} value 需要校验的数据
* @param {boolean} 是否立即校验
* @return {Array|null} 校验内容
*/
async onFieldChange(value, formtrigger = true) {
const {
formData,
localData,
errShowType,
validateCheck,
validateTrigger,
_isRequiredField,
_realName
} = this.form
const name = _realName(this.name)
if (!value) {
value = this.form.formData[name]
}
// fixd by mehaotian 不在校验前清空信息,解决闪屏的问题
// this.errMsg = '';
// fix by mehaotian 解决没有检验规则的情况下,抛出错误的问题
const ruleLen = this.itemRules.rules && this.itemRules.rules.length
if (!this.validator || !ruleLen || ruleLen === 0) return;
// 检验时机
// let trigger = this.isTrigger(this.itemRules.validateTrigger, this.validateTrigger, validateTrigger);
const isRequiredField = _isRequiredField(this.itemRules.rules || []);
let result = null;
// 只有等于 bind 时 ,才能开启时实校验
if (validateTrigger === 'bind' || formtrigger) {
// 校验当前表单项
result = await this.validator.validateUpdate({
[name]: value
},
formData
);
// 判断是否必填,非必填,不填不校验,填写才校验 ,暂时只处理 undefined 和空的情况
if (!isRequiredField && (value === undefined || value === '')) {
result = null;
}
// 判断错误信息显示类型
if (result && result.errorMessage) {
if (errShowType === 'undertext') {
// 获取错误信息
this.errMsg = !result ? '' : result.errorMessage;
}
if (errShowType === 'toast') {
uni.showToast({
title: result.errorMessage || '校验错误',
icon: 'none'
});
}
if (errShowType === 'modal') {
uni.showModal({
title: '提示',
content: result.errorMessage || '校验错误'
});
}
} else {
this.errMsg = ''
}
// 通知 form 组件更新事件
validateCheck(result ? result : null)
} else {
this.errMsg = ''
}
return result ? result : null;
},
/**
* 初始组件数据
*/
init(type = false) {
const {
validator,
formRules,
childrens,
formData,
localData,
_realName,
labelWidth,
_getDataValue,
_setDataValue
} = this.form || {}
// 对齐方式
this.localLabelAlign = this._justifyContent()
// 宽度
this.localLabelWidth = this._labelWidthUnit(labelWidth)
// 标签位置
this.localLabelPos = this._labelPosition()
// 将需要校验的子组件加入form 队列
this.form && type && childrens.push(this)
if (!validator || !formRules) return
// 判断第一个 item
if (!this.form.isFirstBorder) {
this.form.isFirstBorder = true;
this.isFirstBorder = true;
}
// 判断 group 里的第一个 item
if (this.group) {
if (!this.group.isFirstBorder) {
this.group.isFirstBorder = true;
this.isFirstBorder = true;
}
}
this.border = this.form.border;
// 获取子域的真实名称
const name = _realName(this.name)
const itemRule = this.userRules || this.rules
if (typeof formRules === 'object' && itemRule) {
// 子规则替换父规则
formRules[name] = {
rules: itemRule
}
validator.updateSchema(formRules);
}
// 注册校验规则
const itemRules = formRules[name] || {}
this.itemRules = itemRules
// 注册校验函数
this.validator = validator
// 默认值赋予
this.itemSetValue(_getDataValue(this.name, localData))
},
unInit() {
if (this.form) {
const {
childrens,
formData,
_realName
} = this.form
childrens.forEach((item, index) => {
if (item === this) {
this.form.childrens.splice(index, 1)
delete formData[_realName(item.name)]
}
})
}
},
// 设置item 的值
itemSetValue(value) {
const name = this.form._realName(this.name)
const rules = this.itemRules.rules || []
const val = this.form._getValue(name, value, rules)
this.form._setDataValue(name, this.form.formData, val)
return val
},
/**
* 移除该表单项的校验结果
*/
clearValidate() {
this.errMsg = '';
},
// 是否显示星号
_isRequired() {
// TODO 不根据规则显示 星号,考虑后续兼容
// if (this.form) {
// if (this.form._isRequiredField(this.itemRules.rules || []) && this.required) {
// return true
// }
// return false
// }
return this.required
},
// 处理对齐方式
_justifyContent() {
if (this.form) {
const {
labelAlign
} = this.form
let labelAli = this.labelAlign ? this.labelAlign : labelAlign;
if (labelAli === 'left') return 'flex-start';
if (labelAli === 'center') return 'center';
if (labelAli === 'right') return 'flex-end';
}
return 'flex-start';
},
// 处理 label宽度单位 ,继承父元素的值
_labelWidthUnit(labelWidth) {
// if (this.form) {
// const {
// labelWidth
// } = this.form
return this.num2px(this.labelWidth ? this.labelWidth : (labelWidth || (this.label ? 70 : 'auto')))
// }
// return '70px'
},
// 处理 label 位置
_labelPosition() {
if (this.form) return this.form.labelPosition || 'left'
return 'left'
},
/**
* 触发时机
* @param {Object} rule 当前规则内时机
* @param {Object} itemRlue 当前组件时机
* @param {Object} parentRule 父组件时机
*/
isTrigger(rule, itemRlue, parentRule) {
// bind submit
if (rule === 'submit' || !rule) {
if (rule === undefined) {
if (itemRlue !== 'bind') {
if (!itemRlue) {
return parentRule === '' ? 'bind' : 'submit';
}
return 'submit';
}
return 'bind';
}
return 'submit';
}
return 'bind';
},
num2px(num) {
if (typeof num === 'number') {
return `${num}px`
}
return num
}
}
};
</script>
<style lang="scss">
.uni-forms-item {
position: relative;
display: flex;
/* #ifdef APP-NVUE */
// 在 nvue 中,使用 margin-bottom error 信息会被隐藏
padding-bottom: 22px;
/* #endif */
/* #ifndef APP-NVUE */
margin-bottom: 22px;
/* #endif */
flex-direction: row;
&__label {
display: flex;
flex-direction: row;
align-items: center;
text-align: left;
font-size: 14px;
color: #606266;
height: 36px;
padding: 0 12px 0 0;
/* #ifndef APP-NVUE */
vertical-align: middle;
flex-shrink: 0;
/* #endif */
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
&.no-label {
padding: 0;
}
}
&__content {
/* #ifndef MP-TOUTIAO */
// display: flex;
// align-items: center;
/* #endif */
position: relative;
font-size: 14px;
flex: 1;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
flex-direction: row;
/* #ifndef APP || H5 || MP-WEIXIN || APP-NVUE */
// TODO 因为小程序平台会多一层标签节点 ,所以需要在多余节点继承当前样式
&>uni-easyinput,
&>uni-data-picker {
width: 100%;
}
/* #endif */
}
& .uni-forms-item__nuve-content {
display: flex;
flex-direction: column;
flex: 1;
}
&__error {
color: #f56c6c;
font-size: 12px;
line-height: 1;
padding-top: 4px;
position: absolute;
/* #ifndef APP-NVUE */
top: 100%;
left: 0;
transition: transform 0.3s;
transform: translateY(-100%);
/* #endif */
/* #ifdef APP-NVUE */
bottom: 5px;
/* #endif */
opacity: 0;
.error-text {
// 只有 nvue 下这个样式才生效
color: #f56c6c;
font-size: 12px;
}
&.msg--active {
opacity: 1;
transform: translateY(0%);
}
}
// 位置修饰样式
&.is-direction-left {
flex-direction: row;
}
&.is-direction-top {
flex-direction: column;
.uni-forms-item__label {
padding: 0 0 8px;
line-height: 1.5715;
text-align: left;
/* #ifndef APP-NVUE */
white-space: initial;
/* #endif */
}
}
.is-required {
// color: $uni-color-error;
color: #dd524d;
font-weight: bold;
}
}
.uni-forms-item--border {
margin-bottom: 0;
padding: 10px 0;
// padding-bottom: 0;
border-top: 1px #eee solid;
/* #ifndef APP-NVUE */
.uni-forms-item__content {
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
.uni-forms-item__error {
position: relative;
top: 5px;
left: 0;
padding-top: 0;
}
}
/* #endif */
/* #ifdef APP-NVUE */
display: flex;
flex-direction: column;
.uni-forms-item__error {
position: relative;
top: 0px;
left: 0;
padding-top: 0;
margin-top: 5px;
}
/* #endif */
}
.is-first-border {
/* #ifndef APP-NVUE */
border: none;
/* #endif */
/* #ifdef APP-NVUE */
border-width: 0;
/* #endif */
}
</style>

View File

@@ -0,0 +1,404 @@
<template>
<view class="uni-forms">
<form>
<slot></slot>
</form>
</view>
</template>
<script>
import Validator from './validate.js';
import {
deepCopy,
getValue,
isRequiredField,
setDataValue,
getDataValue,
realName,
isRealName,
rawData,
isEqual
} from './utils.js'
// #ifndef VUE3
// 后续会慢慢废弃这个方法
import Vue from 'vue';
Vue.prototype.binddata = function(name, value, formName) {
if (formName) {
this.$refs[formName].setValue(name, value);
} else {
let formVm;
for (let i in this.$refs) {
const vm = this.$refs[i];
if (vm && vm.$options && vm.$options.name === 'uniForms') {
formVm = vm;
break;
}
}
if (!formVm) return console.error('当前 uni-froms 组件缺少 ref 属性');
formVm.setValue(name, value);
}
};
// #endif
/**
* Forms 表单
* @description 由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据
* @tutorial https://ext.dcloud.net.cn/plugin?id=2773
* @property {Object} rules 表单校验规则
* @property {String} validateTrigger = [bind|submit|blur] 校验触发器方式 默认 submit
* @value bind 发生变化时触发
* @value submit 提交时触发
* @value blur 失去焦点时触发
* @property {String} labelPosition = [top|left] label 位置 默认 left
* @value top 顶部显示 label
* @value left 左侧显示 label
* @property {String} labelWidth label 宽度,默认 70px
* @property {String} labelAlign = [left|center|right] label 居中方式 默认 left
* @value left label 左侧显示
* @value center label 居中
* @value right label 右侧对齐
* @property {String} errShowType = [undertext|toast|modal] 校验错误信息提示方式
* @value undertext 错误信息在底部显示
* @value toast 错误信息toast显示
* @value modal 错误信息modal显示
* @event {Function} submit 提交时触发
* @event {Function} validate 校验结果发生变化触发
*/
export default {
name: 'uniForms',
emits: ['validate', 'submit'],
options: {
// #ifdef MP-TOUTIAO
virtualHost: false,
// #endif
// #ifndef MP-TOUTIAO
virtualHost: true
// #endif
},
props: {
// 即将弃用
value: {
type: Object,
default () {
return null;
}
},
// vue3 替换 value 属性
modelValue: {
type: Object,
default () {
return null;
}
},
// 1.4.0 开始将不支持 v-model ,且废弃 value 和 modelValue
model: {
type: Object,
default () {
return null;
}
},
// 表单校验规则
rules: {
type: Object,
default () {
return {};
}
},
//校验错误信息提示方式 默认 undertext 取值 [undertext|toast|modal]
errShowType: {
type: String,
default: 'undertext'
},
// 校验触发器方式 默认 bind 取值 [bind|submit]
validateTrigger: {
type: String,
default: 'submit'
},
// label 位置,默认 left 取值 top/left
labelPosition: {
type: String,
default: 'left'
},
// label 宽度
labelWidth: {
type: [String, Number],
default: ''
},
// label 居中方式,默认 left 取值 left/center/right
labelAlign: {
type: String,
default: 'left'
},
border: {
type: Boolean,
default: false
}
},
provide() {
return {
uniForm: this
}
},
data() {
return {
// 表单本地值的记录,不应该与传如的值进行关联
formData: {},
formRules: {}
};
},
computed: {
// 计算数据源变化的
localData() {
const localVal = this.model || this.modelValue || this.value
if (localVal) {
return deepCopy(localVal)
}
return {}
}
},
watch: {
// 监听数据变化 ,暂时不使用,需要单独赋值
// localData: {},
// 监听规则变化
rules: {
handler: function(val, oldVal) {
this.setRules(val)
},
deep: true,
immediate: true
}
},
created() {
// #ifdef VUE3
let getbinddata = getApp().$vm.$.appContext.config.globalProperties.binddata
if (!getbinddata) {
getApp().$vm.$.appContext.config.globalProperties.binddata = function(name, value, formName) {
if (formName) {
this.$refs[formName].setValue(name, value);
} else {
let formVm;
for (let i in this.$refs) {
const vm = this.$refs[i];
if (vm && vm.$options && vm.$options.name === 'uniForms') {
formVm = vm;
break;
}
}
if (!formVm) return console.error('当前 uni-froms 组件缺少 ref 属性');
if(formVm.model)formVm.model[name] = value
if(formVm.modelValue)formVm.modelValue[name] = value
if(formVm.value)formVm.value[name] = value
}
}
}
// #endif
// 子组件实例数组
this.childrens = []
// TODO 兼容旧版 uni-data-picker ,新版本中无效,只是避免报错
this.inputChildrens = []
this.setRules(this.rules)
},
methods: {
/**
* 外部调用方法
* 设置规则 ,主要用于小程序自定义检验规则
* @param {Array} rules 规则源数据
*/
setRules(rules) {
// TODO 有可能子组件合并规则的时机比这个要早,所以需要合并对象 ,而不是直接赋值,可能会被覆盖
this.formRules = Object.assign({}, this.formRules, rules)
// 初始化校验函数
this.validator = new Validator(rules);
},
/**
* 外部调用方法
* 设置数据,用于设置表单数据,公开给用户使用 不支持在动态表单中使用
* @param {Object} key
* @param {Object} value
*/
setValue(key, value) {
let example = this.childrens.find(child => child.name === key);
if (!example) return null;
this.formData[key] = getValue(key, value, (this.formRules[key] && this.formRules[key].rules) || [])
return example.onFieldChange(this.formData[key]);
},
/**
* 外部调用方法
* 手动提交校验表单
* 对整个表单进行校验的方法,参数为一个回调函数。
* @param {Array} keepitem 保留不参与校验的字段
* @param {type} callback 方法回调
*/
validate(keepitem, callback) {
return this.checkAll(this.formData, keepitem, callback);
},
/**
* 外部调用方法
* 部分表单校验
* @param {Array|String} props 需要校验的字段
* @param {Function} 回调函数
*/
validateField(props = [], callback) {
props = [].concat(props);
let invalidFields = {};
this.childrens.forEach(item => {
const name = realName(item.name)
if (props.indexOf(name) !== -1) {
invalidFields = Object.assign({}, invalidFields, {
[name]: this.formData[name]
});
}
});
return this.checkAll(invalidFields, [], callback);
},
/**
* 外部调用方法
* 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组,如不传则移除整个表单的校验结果
* @param {Array|String} props 需要移除校验的字段 ,不填为所有
*/
clearValidate(props = []) {
props = [].concat(props);
this.childrens.forEach(item => {
if (props.length === 0) {
item.errMsg = '';
} else {
const name = realName(item.name)
if (props.indexOf(name) !== -1) {
item.errMsg = '';
}
}
});
},
/**
* 外部调用方法 ,即将废弃
* 手动提交校验表单
* 对整个表单进行校验的方法,参数为一个回调函数。
* @param {Array} keepitem 保留不参与校验的字段
* @param {type} callback 方法回调
*/
submit(keepitem, callback, type) {
for (let i in this.dataValue) {
const itemData = this.childrens.find(v => v.name === i);
if (itemData) {
if (this.formData[i] === undefined) {
this.formData[i] = this._getValue(i, this.dataValue[i]);
}
}
}
if (!type) {
console.warn('submit 方法即将废弃请使用validate方法代替');
}
return this.checkAll(this.formData, keepitem, callback, 'submit');
},
// 校验所有
async checkAll(invalidFields, keepitem, callback, type) {
// 不存在校验规则 ,则停止校验流程
if (!this.validator) return
let childrens = []
// 处理参与校验的item实例
for (let i in invalidFields) {
const item = this.childrens.find(v => realName(v.name) === i)
if (item) {
childrens.push(item)
}
}
// 如果validate第一个参数是funciont ,那就走回调
if (!callback && typeof keepitem === 'function') {
callback = keepitem;
}
let promise;
// 如果不存在回调,那么使用 Promise 方式返回
if (!callback && typeof callback !== 'function' && Promise) {
promise = new Promise((resolve, reject) => {
callback = function(valid, invalidFields) {
!valid ? resolve(invalidFields) : reject(valid);
};
});
}
let results = [];
// 避免引用错乱 ,建议拷贝对象处理
let tempFormData = JSON.parse(JSON.stringify(invalidFields))
// 所有子组件参与校验,使用 for 可以使用 awiat
for (let i in childrens) {
const child = childrens[i]
let name = realName(child.name);
const result = await child.onFieldChange(tempFormData[name]);
if (result) {
results.push(result);
// toast ,modal 只需要执行第一次就可以
if (this.errShowType === 'toast' || this.errShowType === 'modal') break;
}
}
if (Array.isArray(results)) {
if (results.length === 0) results = null;
}
if (Array.isArray(keepitem)) {
keepitem.forEach(v => {
let vName = realName(v);
let value = getDataValue(v, this.localData)
if (value !== undefined) {
tempFormData[vName] = value
}
});
}
// TODO submit 即将废弃
if (type === 'submit') {
this.$emit('submit', {
detail: {
value: tempFormData,
errors: results
}
});
} else {
this.$emit('validate', results);
}
// const resetFormData = rawData(tempFormData, this.localData, this.name)
let resetFormData = {}
resetFormData = rawData(tempFormData, this.name)
callback && typeof callback === 'function' && callback(results, resetFormData);
if (promise && callback) {
return promise;
} else {
return null;
}
},
/**
* 返回validate事件
* @param {Object} result
*/
validateCheck(result) {
this.$emit('validate', result);
},
_getValue: getValue,
_isRequiredField: isRequiredField,
_setDataValue: setDataValue,
_getDataValue: getDataValue,
_realName: realName,
_isRealName: isRealName,
_isEqual: isEqual
}
};
</script>
<style lang="scss">
.uni-forms {}
</style>

View File

@@ -0,0 +1,293 @@
/**
* 简单处理对象拷贝
* @param {Obejct} 被拷贝对象
* @@return {Object} 拷贝对象
*/
export const deepCopy = (val) => {
return JSON.parse(JSON.stringify(val))
}
/**
* 过滤数字类型
* @param {String} format 数字类型
* @@return {Boolean} 返回是否为数字类型
*/
export const typeFilter = (format) => {
return format === 'int' || format === 'double' || format === 'number' || format === 'timestamp';
}
/**
* 把 value 转换成指定的类型,用于处理初始值,原因是初始值需要入库不能为 undefined
* @param {String} key 字段名
* @param {any} value 字段值
* @param {Object} rules 表单校验规则
*/
export const getValue = (key, value, rules) => {
const isRuleNumType = rules.find(val => val.format && typeFilter(val.format));
const isRuleBoolType = rules.find(val => (val.format && val.format === 'boolean') || val.format === 'bool');
// 输入类型为 number
if (!!isRuleNumType) {
if (!value && value !== 0) {
value = null
} else {
value = isNumber(Number(value)) ? Number(value) : value
}
}
// 输入类型为 boolean
if (!!isRuleBoolType) {
value = isBoolean(value) ? value : false
}
return value;
}
/**
* 获取表单数据
* @param {String|Array} name 真实名称,需要使用 realName 获取
* @param {Object} data 原始数据
* @param {any} value 需要设置的值
*/
export const setDataValue = (field, formdata, value) => {
formdata[field] = value
return value || ''
}
/**
* 获取表单数据
* @param {String|Array} field 真实名称,需要使用 realName 获取
* @param {Object} data 原始数据
*/
export const getDataValue = (field, data) => {
return objGet(data, field)
}
/**
* 获取表单类型
* @param {String|Array} field 真实名称,需要使用 realName 获取
*/
export const getDataValueType = (field, data) => {
const value = getDataValue(field, data)
return {
type: type(value),
value
}
}
/**
* 获取表单可用的真实name
* @param {String|Array} name 表单name
* @@return {String} 表单可用的真实name
*/
export const realName = (name, data = {}) => {
const base_name = _basePath(name)
if (typeof base_name === 'object' && Array.isArray(base_name) && base_name.length > 1) {
const realname = base_name.reduce((a, b) => a += `#${b}`, '_formdata_')
return realname
}
return base_name[0] || name
}
/**
* 判断是否表单可用的真实name
* @param {String|Array} name 表单name
* @@return {String} 表单可用的真实name
*/
export const isRealName = (name) => {
const reg = /^_formdata_#*/
return reg.test(name)
}
/**
* 获取表单数据的原始格式
* @@return {Object|Array} object 需要解析的数据
*/
export const rawData = (object = {}, name) => {
let newData = JSON.parse(JSON.stringify(object))
let formData = {}
for(let i in newData){
let path = name2arr(i)
objSet(formData,path,newData[i])
}
return formData
}
/**
* 真实name还原为 array
* @param {*} name
*/
export const name2arr = (name) => {
let field = name.replace('_formdata_#', '')
field = field.split('#').map(v => (isNumber(v) ? Number(v) : v))
return field
}
/**
* 对象中设置值
* @param {Object|Array} object 源数据
* @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c']
* @param {String} value 需要设置的值
*/
export const objSet = (object, path, value) => {
if (typeof object !== 'object') return object;
_basePath(path).reduce((o, k, i, _) => {
if (i === _.length - 1) {
// 若遍历结束直接赋值
o[k] = value
return null
} else if (k in o) {
// 若存在对应路径,则返回找到的对象,进行下一次遍历
return o[k]
} else {
// 若不存在对应路径,则创建对应对象,若下一路径是数字,新对象赋值为空数组,否则赋值为空对象
o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {}
return o[k]
}
}, object)
// 返回object
return object;
}
// 处理 path path有三种形式'a[0].b.c'、'a.0.b.c' 和 ['a','0','b','c'],需要统一处理成数组,便于后续使用
function _basePath(path) {
// 若是数组,则直接返回
if (Array.isArray(path)) return path
// 若有 '[',']',则替换成将 '[' 替换成 '.',去掉 ']'
return path.replace(/\[/g, '.').replace(/\]/g, '').split('.')
}
/**
* 从对象中获取值
* @param {Object|Array} object 源数据
* @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c']
* @param {String} defaultVal 如果无法从调用链中获取值的默认值
*/
export const objGet = (object, path, defaultVal = 'undefined') => {
// 先将path处理成统一格式
let newPath = _basePath(path)
// 递归处理,返回最后结果
let val = newPath.reduce((o, k) => {
return (o || {})[k]
}, object);
return !val || val !== undefined ? val : defaultVal
}
/**
* 是否为 number 类型
* @param {any} num 需要判断的值
* @return {Boolean} 是否为 number
*/
export const isNumber = (num) => {
return !isNaN(Number(num))
}
/**
* 是否为 boolean 类型
* @param {any} bool 需要判断的值
* @return {Boolean} 是否为 boolean
*/
export const isBoolean = (bool) => {
return (typeof bool === 'boolean')
}
/**
* 是否有必填字段
* @param {Object} rules 规则
* @return {Boolean} 是否有必填字段
*/
export const isRequiredField = (rules) => {
let isNoField = false;
for (let i = 0; i < rules.length; i++) {
const ruleData = rules[i];
if (ruleData.required) {
isNoField = true;
break;
}
}
return isNoField;
}
/**
* 获取数据类型
* @param {Any} obj 需要获取数据类型的值
*/
export const type = (obj) => {
var class2type = {};
// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {
class2type["[object " + item + "]"] = item.toLowerCase();
})
if (obj == null) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[Object.prototype.toString.call(obj)] || "object" :
typeof obj;
}
/**
* 判断两个值是否相等
* @param {any} a 值
* @param {any} b 值
* @return {Boolean} 是否相等
*/
export const isEqual = (a, b) => {
//如果a和b本来就全等
if (a === b) {
//判断是否为0和-0
return a !== 0 || 1 / a === 1 / b;
}
//判断是否为null和undefined
if (a == null || b == null) {
return a === b;
}
//接下来判断a和b的数据类型
var classNameA = toString.call(a),
classNameB = toString.call(b);
//如果数据类型不相等则返回false
if (classNameA !== classNameB) {
return false;
}
//如果数据类型相等,再根据不同数据类型分别判断
switch (classNameA) {
case '[object RegExp]':
case '[object String]':
//进行字符串转换比较
return '' + a === '' + b;
case '[object Number]':
//进行数字转换比较,判断是否为NaN
if (+a !== +a) {
return +b !== +b;
}
//判断是否为0或-0
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]':
case '[object Boolean]':
return +a === +b;
}
//如果是对象类型
if (classNameA == '[object Object]') {
//获取a和b的属性长度
var propsA = Object.getOwnPropertyNames(a),
propsB = Object.getOwnPropertyNames(b);
if (propsA.length != propsB.length) {
return false;
}
for (var i = 0; i < propsA.length; i++) {
var propName = propsA[i];
//如果对应属性对应值不相等则返回false
if (a[propName] !== b[propName]) {
return false;
}
}
return true;
}
//如果是数组类型
if (classNameA == '[object Array]') {
if (a.toString() == b.toString()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,486 @@
var pattern = {
email: /^\S+?@\S+?\.\S+?$/,
idcard: /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/,
url: new RegExp(
"^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$",
'i')
};
const FORMAT_MAPPING = {
"int": 'integer',
"bool": 'boolean',
"double": 'number',
"long": 'number',
"password": 'string'
// "fileurls": 'array'
}
function formatMessage(args, resources = '') {
var defaultMessage = ['label']
defaultMessage.forEach((item) => {
if (args[item] === undefined) {
args[item] = ''
}
})
let str = resources
for (let key in args) {
let reg = new RegExp('{' + key + '}')
str = str.replace(reg, args[key])
}
return str
}
function isEmptyValue(value, type) {
if (value === undefined || value === null) {
return true;
}
if (typeof value === 'string' && !value) {
return true;
}
if (Array.isArray(value) && !value.length) {
return true;
}
if (type === 'object' && !Object.keys(value).length) {
return true;
}
return false;
}
const types = {
integer(value) {
return types.number(value) && parseInt(value, 10) === value;
},
string(value) {
return typeof value === 'string';
},
number(value) {
if (isNaN(value)) {
return false;
}
return typeof value === 'number';
},
"boolean": function(value) {
return typeof value === 'boolean';
},
"float": function(value) {
return types.number(value) && !types.integer(value);
},
array(value) {
return Array.isArray(value);
},
object(value) {
return typeof value === 'object' && !types.array(value);
},
date(value) {
return value instanceof Date;
},
timestamp(value) {
if (!this.integer(value) || Math.abs(value).toString().length > 16) {
return false
}
return true;
},
file(value) {
return typeof value.url === 'string';
},
email(value) {
return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255;
},
url(value) {
return typeof value === 'string' && !!value.match(pattern.url);
},
pattern(reg, value) {
try {
return new RegExp(reg).test(value);
} catch (e) {
return false;
}
},
method(value) {
return typeof value === 'function';
},
idcard(value) {
return typeof value === 'string' && !!value.match(pattern.idcard);
},
'url-https'(value) {
return this.url(value) && value.startsWith('https://');
},
'url-scheme'(value) {
return value.startsWith('://');
},
'url-web'(value) {
return false;
}
}
class RuleValidator {
constructor(message) {
this._message = message
}
async validateRule(fieldKey, fieldValue, value, data, allData) {
var result = null
let rules = fieldValue.rules
let hasRequired = rules.findIndex((item) => {
return item.required
})
if (hasRequired < 0) {
if (value === null || value === undefined) {
return result
}
if (typeof value === 'string' && !value.length) {
return result
}
}
var message = this._message
if (rules === undefined) {
return message['default']
}
for (var i = 0; i < rules.length; i++) {
let rule = rules[i]
let vt = this._getValidateType(rule)
Object.assign(rule, {
label: fieldValue.label || `["${fieldKey}"]`
})
if (RuleValidatorHelper[vt]) {
result = RuleValidatorHelper[vt](rule, value, message)
if (result != null) {
break
}
}
if (rule.validateExpr) {
let now = Date.now()
let resultExpr = rule.validateExpr(value, allData, now)
if (resultExpr === false) {
result = this._getMessage(rule, rule.errorMessage || this._message['default'])
break
}
}
if (rule.validateFunction) {
result = await this.validateFunction(rule, value, data, allData, vt)
if (result !== null) {
break
}
}
}
if (result !== null) {
result = message.TAG + result
}
return result
}
async validateFunction(rule, value, data, allData, vt) {
let result = null
try {
let callbackMessage = null
const res = await rule.validateFunction(rule, value, allData || data, (message) => {
callbackMessage = message
})
if (callbackMessage || (typeof res === 'string' && res) || res === false) {
result = this._getMessage(rule, callbackMessage || res, vt)
}
} catch (e) {
result = this._getMessage(rule, e.message, vt)
}
return result
}
_getMessage(rule, message, vt) {
return formatMessage(rule, message || rule.errorMessage || this._message[vt] || message['default'])
}
_getValidateType(rule) {
var result = ''
if (rule.required) {
result = 'required'
} else if (rule.format) {
result = 'format'
} else if (rule.arrayType) {
result = 'arrayTypeFormat'
} else if (rule.range) {
result = 'range'
} else if (rule.maximum !== undefined || rule.minimum !== undefined) {
result = 'rangeNumber'
} else if (rule.maxLength !== undefined || rule.minLength !== undefined) {
result = 'rangeLength'
} else if (rule.pattern) {
result = 'pattern'
} else if (rule.validateFunction) {
result = 'validateFunction'
}
return result
}
}
const RuleValidatorHelper = {
required(rule, value, message) {
if (rule.required && isEmptyValue(value, rule.format || typeof value)) {
return formatMessage(rule, rule.errorMessage || message.required);
}
return null
},
range(rule, value, message) {
const {
range,
errorMessage
} = rule;
let list = new Array(range.length);
for (let i = 0; i < range.length; i++) {
const item = range[i];
if (types.object(item) && item.value !== undefined) {
list[i] = item.value;
} else {
list[i] = item;
}
}
let result = false
if (Array.isArray(value)) {
result = (new Set(value.concat(list)).size === list.length);
} else {
if (list.indexOf(value) > -1) {
result = true;
}
}
if (!result) {
return formatMessage(rule, errorMessage || message['enum']);
}
return null
},
rangeNumber(rule, value, message) {
if (!types.number(value)) {
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch);
}
let {
minimum,
maximum,
exclusiveMinimum,
exclusiveMaximum
} = rule;
let min = exclusiveMinimum ? value <= minimum : value < minimum;
let max = exclusiveMaximum ? value >= maximum : value > maximum;
if (minimum !== undefined && min) {
return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMinimum ?
'exclusiveMinimum' : 'minimum'
])
} else if (maximum !== undefined && max) {
return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMaximum ?
'exclusiveMaximum' : 'maximum'
])
} else if (minimum !== undefined && maximum !== undefined && (min || max)) {
return formatMessage(rule, rule.errorMessage || message['number'].range)
}
return null
},
rangeLength(rule, value, message) {
if (!types.string(value) && !types.array(value)) {
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch);
}
let min = rule.minLength;
let max = rule.maxLength;
let val = value.length;
if (min !== undefined && val < min) {
return formatMessage(rule, rule.errorMessage || message['length'].minLength)
} else if (max !== undefined && val > max) {
return formatMessage(rule, rule.errorMessage || message['length'].maxLength)
} else if (min !== undefined && max !== undefined && (val < min || val > max)) {
return formatMessage(rule, rule.errorMessage || message['length'].range)
}
return null
},
pattern(rule, value, message) {
if (!types['pattern'](rule.pattern, value)) {
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch);
}
return null
},
format(rule, value, message) {
var customTypes = Object.keys(types);
var format = FORMAT_MAPPING[rule.format] ? FORMAT_MAPPING[rule.format] : (rule.format || rule.arrayType);
if (customTypes.indexOf(format) > -1) {
if (!types[format](value)) {
return formatMessage(rule, rule.errorMessage || message.typeError);
}
}
return null
},
arrayTypeFormat(rule, value, message) {
if (!Array.isArray(value)) {
return formatMessage(rule, rule.errorMessage || message.typeError);
}
for (let i = 0; i < value.length; i++) {
const element = value[i];
let formatResult = this.format(rule, element, message)
if (formatResult !== null) {
return formatResult
}
}
return null
}
}
class SchemaValidator extends RuleValidator {
constructor(schema, options) {
super(SchemaValidator.message);
this._schema = schema
this._options = options || null
}
updateSchema(schema) {
this._schema = schema
}
async validate(data, allData) {
let result = this._checkFieldInSchema(data)
if (!result) {
result = await this.invokeValidate(data, false, allData)
}
return result.length ? result[0] : null
}
async validateAll(data, allData) {
let result = this._checkFieldInSchema(data)
if (!result) {
result = await this.invokeValidate(data, true, allData)
}
return result
}
async validateUpdate(data, allData) {
let result = this._checkFieldInSchema(data)
if (!result) {
result = await this.invokeValidateUpdate(data, false, allData)
}
return result.length ? result[0] : null
}
async invokeValidate(data, all, allData) {
let result = []
let schema = this._schema
for (let key in schema) {
let value = schema[key]
let errorMessage = await this.validateRule(key, value, data[key], data, allData)
if (errorMessage != null) {
result.push({
key,
errorMessage
})
if (!all) break
}
}
return result
}
async invokeValidateUpdate(data, all, allData) {
let result = []
for (let key in data) {
let errorMessage = await this.validateRule(key, this._schema[key], data[key], data, allData)
if (errorMessage != null) {
result.push({
key,
errorMessage
})
if (!all) break
}
}
return result
}
_checkFieldInSchema(data) {
var keys = Object.keys(data)
var keys2 = Object.keys(this._schema)
if (new Set(keys.concat(keys2)).size === keys2.length) {
return ''
}
var noExistFields = keys.filter((key) => {
return keys2.indexOf(key) < 0;
})
var errorMessage = formatMessage({
field: JSON.stringify(noExistFields)
}, SchemaValidator.message.TAG + SchemaValidator.message['defaultInvalid'])
return [{
key: 'invalid',
errorMessage
}]
}
}
function Message() {
return {
TAG: "",
default: '验证错误',
defaultInvalid: '提交的字段{field}在数据库中并不存在',
validateFunction: '验证无效',
required: '{label}必填',
'enum': '{label}超出范围',
timestamp: '{label}格式无效',
whitespace: '{label}不能为空',
typeError: '{label}类型无效',
date: {
format: '{label}日期{value}格式无效',
parse: '{label}日期无法解析,{value}无效',
invalid: '{label}日期{value}无效'
},
length: {
minLength: '{label}长度不能少于{minLength}',
maxLength: '{label}长度不能超过{maxLength}',
range: '{label}必须介于{minLength}和{maxLength}之间'
},
number: {
minimum: '{label}不能小于{minimum}',
maximum: '{label}不能大于{maximum}',
exclusiveMinimum: '{label}不能小于等于{minimum}',
exclusiveMaximum: '{label}不能大于等于{maximum}',
range: '{label}必须介于{minimum}and{maximum}之间'
},
pattern: {
mismatch: '{label}格式不匹配'
}
};
}
SchemaValidator.message = new Message();
export default SchemaValidator

View File

@@ -0,0 +1,89 @@
{
"id": "uni-forms",
"displayName": "uni-forms 表单",
"version": "1.4.13",
"description": "由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据",
"keywords": [
"uni-ui",
"表单",
"校验",
"表单校验",
"表单验证"
],
"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",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
## Forms 表单
> **组件名uni-forms**
> 代码块: `uForms`、`uni-forms-item`
> 关联组件:`uni-forms-item`、`uni-easyinput`、`uni-data-checkbox`、`uni-group`。
uni-app的内置组件已经有了 `<form>`组件,用于提交表单内容。
然而几乎每个表单都需要做表单验证,为了方便做表单验证,减少重复开发,`uni ui` 又基于 `<form>`组件封装了 `<uni-forms>`组件,内置了表单验证功能。
`<uni-forms>` 提供了 `rules`属性来描述校验规则、`<uni-forms-item>`子组件来包裹具体的表单项,以及给原生或三方组件提供了 `binddata()` 来设置表单值。
每个要校验的表单项不管input还是checkbox都必须放在`<uni-forms-item>`组件中,且一个`<uni-forms-item>`组件只能放置一个表单项。
`<uni-forms-item>`组件内部预留了显示error message的区域默认是在表单项的底部。
另外,`<uni-forms>`组件下面的各个表单项,可以通过`<uni-group>`包裹为不同的分组。同一`<uni-group>`下的不同表单项目将聚拢在一起同其他group保持垂直间距。`<uni-group>`仅影响视觉效果。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-forms)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@@ -120,7 +120,7 @@
.uni-tag {
line-height: 14px;
font-size: 12px;
font-size: 12rpx;
font-weight: 200;
padding: $tag-default-pd;
color: #fff;
@@ -151,7 +151,7 @@
&--mini {
padding: $tag-mini-pd;
font-size: 12px;
font-size: 8px !important;
border-radius: 2px;
}