库存盘点和项目的初版
This commit is contained in:
45
api/oa/oaContract.js
Normal file
45
api/oa/oaContract.js
Normal 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
10
api/oa/wms/stock.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import request from "@/util/oaRequest"
|
||||
|
||||
// 查询库存管理列表
|
||||
export function listStock(query) {
|
||||
return request({
|
||||
url: '/oa/oaWarehouse/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
32
pages.json
32
pages.json
@@ -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": {
|
||||
|
||||
@@ -26,7 +26,16 @@
|
||||
<image class="entry-icon" src="/static/images/express.svg" mode="aspectFit"></image>
|
||||
<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>
|
||||
|
||||
139
pages/workbench/project/add.vue
Normal file
139
pages/workbench/project/add.vue
Normal 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>
|
||||
197
pages/workbench/project/detail.vue
Normal file
197
pages/workbench/project/detail.vue
Normal 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>
|
||||
254
pages/workbench/project/project.vue
Normal file
254
pages/workbench/project/project.vue
Normal 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
385
pages/workbench/wms/wms.vue
Normal 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
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
BIN
static/images/stock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
100
uni_modules/uni-forms/changelog.md
Normal file
100
uni_modules/uni-forms/changelog.md
Normal file
@@ -0,0 +1,100 @@
|
||||
## 1.4.13(2024-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.10(2023-11-03)
|
||||
- 优化 labelWidth 描述错误
|
||||
## 1.4.9(2023-02-10)
|
||||
- 修复 required 参数无法动态绑定
|
||||
## 1.4.8(2022-08-23)
|
||||
- 优化 根据 rules 自动添加 required 的问题
|
||||
## 1.4.7(2022-08-22)
|
||||
- 修复 item 未设置 require 属性,rules 设置 require 后,星号也显示的 bug,详见:[https://ask.dcloud.net.cn/question/151540](https://ask.dcloud.net.cn/question/151540)
|
||||
## 1.4.6(2022-07-13)
|
||||
- 修复 model 需要校验的值没有声明对应字段时,导致第一次不触发校验的bug
|
||||
## 1.4.5(2022-07-05)
|
||||
- 新增 更多表单示例
|
||||
- 优化 子表单组件过期提示的问题
|
||||
- 优化 子表单组件uni-datetime-picker、uni-data-select、uni-data-picker的显示样式
|
||||
## 1.4.4(2022-07-04)
|
||||
- 更新 删除组件日志
|
||||
## 1.4.3(2022-07-04)
|
||||
- 修复 由 1.4.0 引发的 label 插槽不生效的bug
|
||||
## 1.4.2(2022-07-04)
|
||||
- 修复 子组件找不到 setValue 报错的bug
|
||||
## 1.4.1(2022-07-04)
|
||||
- 修复 uni-data-picker 在 uni-forms-item 中报错的bug
|
||||
- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug
|
||||
## 1.4.0(2022-06-30)
|
||||
- 【重要】组件逻辑重构,部分用法用旧版本不兼容,请注意兼容问题
|
||||
- 【重要】组件使用 Provide/Inject 方式注入依赖,提供了自定义表单组件调用 uni-forms 校验表单的能力
|
||||
- 新增 model 属性,等同于原 value/modelValue 属性,旧属性即将废弃
|
||||
- 新增 validateTrigger 属性的 blur 值,仅 uni-easyinput 生效
|
||||
- 新增 onFieldChange 方法,可以对子表单进行校验,可替代binddata方法
|
||||
- 新增 子表单的 setRules 方法,配合自定义校验函数使用
|
||||
- 新增 uni-forms-item 的 setRules 方法,配置动态表单使用可动态更新校验规则
|
||||
- 优化 动态表单校验方式,废弃拼接name的方式
|
||||
## 1.3.3(2022-06-22)
|
||||
- 修复 表单校验顺序无序问题
|
||||
## 1.3.2(2021-12-09)
|
||||
-
|
||||
## 1.3.1(2021-11-19)
|
||||
- 修复 label 插槽不生效的bug
|
||||
## 1.3.0(2021-11-19)
|
||||
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
|
||||
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-forms](https://uniapp.dcloud.io/component/uniui/uni-forms)
|
||||
## 1.2.7(2021-08-13)
|
||||
- 修复 没有添加校验规则的字段依然报错的Bug
|
||||
## 1.2.6(2021-08-11)
|
||||
- 修复 重置表单错误信息无法清除的问题
|
||||
## 1.2.5(2021-08-11)
|
||||
- 优化 组件文档
|
||||
## 1.2.4(2021-08-11)
|
||||
- 修复 表单验证只生效一次的问题
|
||||
## 1.2.3(2021-07-30)
|
||||
- 优化 vue3下事件警告的问题
|
||||
## 1.2.2(2021-07-26)
|
||||
- 修复 vue2 下条件编译导致destroyed生命周期失效的Bug
|
||||
- 修复 1.2.1 引起的示例在小程序平台报错的Bug
|
||||
## 1.2.1(2021-07-22)
|
||||
- 修复 动态校验表单,默认值为空的情况下校验失效的Bug
|
||||
- 修复 不指定name属性时,运行报错的Bug
|
||||
- 优化 label默认宽度从65调整至70,使required为true且四字时不换行
|
||||
- 优化 组件示例,新增动态校验示例代码
|
||||
- 优化 组件文档,使用方式更清晰
|
||||
## 1.2.0(2021-07-13)
|
||||
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
|
||||
## 1.1.2(2021-06-25)
|
||||
- 修复 pattern 属性在微信小程序平台无效的问题
|
||||
## 1.1.1(2021-06-22)
|
||||
- 修复 validate-trigger属性为submit且err-show-type属性为toast时不能弹出的Bug
|
||||
## 1.1.0(2021-06-22)
|
||||
- 修复 只写setRules方法而导致校验不生效的Bug
|
||||
- 修复 由上个办法引发的错误提示文字错位的Bug
|
||||
## 1.0.48(2021-06-21)
|
||||
- 修复 不设置 label 属性 ,无法设置label插槽的问题
|
||||
## 1.0.47(2021-06-21)
|
||||
- 修复 不设置label属性,label-width属性不生效的bug
|
||||
- 修复 setRules 方法与rules属性冲突的问题
|
||||
## 1.0.46(2021-06-04)
|
||||
- 修复 动态删减数据导致报错的问题
|
||||
## 1.0.45(2021-06-04)
|
||||
- 新增 modelValue 属性 ,value 即将废弃
|
||||
## 1.0.44(2021-06-02)
|
||||
- 新增 uni-forms-item 可以设置单独的 rules
|
||||
- 新增 validate 事件增加 keepitem 参数,可以选择那些字段不过滤
|
||||
- 优化 submit 事件重命名为 validate
|
||||
## 1.0.43(2021-05-12)
|
||||
- 新增 组件示例地址
|
||||
## 1.0.42(2021-04-30)
|
||||
- 修复 自定义检验器失效的问题
|
||||
## 1.0.41(2021-03-05)
|
||||
- 更新 校验器
|
||||
- 修复 表单规则设置类型为 number 的情况下,值为0校验失败的Bug
|
||||
## 1.0.40(2021-03-04)
|
||||
- 修复 动态显示uni-forms-item的情况下,submit 方法获取值错误的Bug
|
||||
## 1.0.39(2021-02-05)
|
||||
- 调整为uni_modules目录规范
|
||||
- 修复 校验器传入 int 等类型 ,返回String类型的Bug
|
||||
@@ -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>
|
||||
404
uni_modules/uni-forms/components/uni-forms/uni-forms.vue
Normal file
404
uni_modules/uni-forms/components/uni-forms/uni-forms.vue
Normal 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>
|
||||
293
uni_modules/uni-forms/components/uni-forms/utils.js
Normal file
293
uni_modules/uni-forms/components/uni-forms/utils.js
Normal 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;
|
||||
}
|
||||
}
|
||||
486
uni_modules/uni-forms/components/uni-forms/validate.js
Normal file
486
uni_modules/uni-forms/components/uni-forms/validate.js
Normal 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
|
||||
89
uni_modules/uni-forms/package.json
Normal file
89
uni_modules/uni-forms/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
uni_modules/uni-forms/readme.md
Normal file
23
uni_modules/uni-forms/readme.md
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user