移动端V1.0

This commit is contained in:
2025-07-16 15:34:34 +08:00
commit 194b0750fd
1083 changed files with 178295 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
<template>
<view>
<picker mode="multiSelector" :disabled="disabled" :value="multiIndex" :range="multiRange"
@change="onMultiChange" @tap="onPickerTap">
<view class="picker">
<input type="text" :value="formattedDateTime" readonly />
<text v-if="!disabled" class="picker-label">选择时间</text>
</view>
</picker>
</view>
</template>
<script>
export default {
props: {
value: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
defaultTime: {
type: String,
default: () => {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
}
}
},
data() {
const now = new Date();
return {
multiIndex: [],
multiRange: this.getMultiRange()
};
},
computed: {
formattedDateTime() {
if (this.multiIndex.length === 0) return ''; // 未选择时不显示
const year = this.multiRange[0][this.multiIndex[0]];
const month = String(this.multiRange[1][this.multiIndex[1]]).padStart(2, '0');
const day = String(this.multiRange[2][this.multiIndex[2]]).padStart(2, '0');
const hour = String(this.multiRange[3][this.multiIndex[3]]).padStart(2, '0');
const minute = String(this.multiRange[4][this.multiIndex[4]]).padStart(2, '0');
const second = String(this.multiRange[5][this.multiIndex[5]]).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
},
methods: {
getMultiRange() {
let yearRange = [];
let monthRange = [];
let dayRange = [];
let hourRange = [];
let minuteRange = [];
let secondRange = [];
for (let i = 1900; i <= 2100; i++) yearRange.push(i);
for (let i = 1; i <= 12; i++) monthRange.push(i);
for (let i = 1; i <= 31; i++) dayRange.push(i);
for (let i = 0; i <= 23; i++) hourRange.push(i);
for (let i = 0; i <= 59; i++) minuteRange.push(i);
for (let i = 0; i <= 59; i++) secondRange.push(i);
return [yearRange, monthRange, dayRange, hourRange, minuteRange, secondRange];
},
onMultiChange(e) {
this.multiIndex = e.detail.value;
this.$emit('input', this.formattedDateTime);
},
onPickerTap() {
// 如果还未选择,则用当前时间作为默认
if (this.multiIndex.length === 0) {
const now = new Date();
this.multiIndex = [
now.getFullYear() - 1900,
now.getMonth(),
now.getDate() - 1,
now.getHours(),
now.getMinutes(),
now.getSeconds()
];
}
}
},
watch: {
value: {
immediate: true, // 组件挂载时立即执行
handler(newValue) {
if (!newValue || typeof newValue !== 'string') return;
const dateParts = newValue.split(/[- :]/);
if (dateParts.length === 6) {
this.multiIndex = [
parseInt(dateParts[0]) - 1900,
parseInt(dateParts[1]) - 1,
parseInt(dateParts[2]) - 1,
parseInt(dateParts[3]),
parseInt(dateParts[4]),
parseInt(dateParts[5])
];
}
}
}
}
};
</script>
<style>
.picker {
display: flex;
align-items: center;
padding: 10px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
/* 使其看起来像可点击的按钮 */
}
input {
flex: 1;
border: none;
outline: none;
padding: 0;
}
.picker-label {
margin-left: 10px;
color: #007BFF;
/* 可选:设置文字颜色 */
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
<template>
<view style="background-color: white;padding: 5rpx;">
<view class="center">
<view style="margin-right: 20rpx;">
{{label}}:
</view>
<uni-easyinput style="padding: 5rpx;" @focus="open" trim="all" v-model="selctValue"
placeholder="请选择"></uni-easyinput>
</view>
<uni-popup ref="popup" type="bottom">
<view style="height: 60vh;background-color: white;padding: 20rpx;padding-top: 40rpx;">
<uni-row>
<uni-col :span="4" :offset="0">
<view class="demo-uni-col center">
<span @click.stop="cancle" style="color: rgba(69, 90, 100, 0.6);">
</span>
</view>
</uni-col>
<uni-col :span="14" :offset="1">
<view class="center" v-if="filterable">
<uni-easyinput v-model="search" focus placeholder="输入关键字过滤"
@input="inputChange"></uni-easyinput>
</view>
</uni-col>
<uni-col :span="4" :offset="filterable ?1:15">
<view class="demo-uni-col center" @click="confirm" style="color: #576b95;"> </view>
</uni-col>
</uni-row>
<uni-row>
<uni-col :span="8">
<view class="">
<picker-view @change="bindChange" class="picker-view">
<!-- -->
<picker-view-column>
<view class="center" v-for="(item,index) in provinces" :key="Math.random()">
{{item.value}}
</view>
</picker-view-column>
<!-- -->
<picker-view-column>
<view class="center" v-for="(item,index) in cities" :key="Math.random()">
{{item.value}}
</view>
</picker-view-column>
区县
<picker-view-column>
<view class="center" v-for="(item,index) in counties" :key="Math.random()">
{{item.value}}
</view>
</picker-view-column>
</picker-view>
</view>
</uni-col>
</uni-row>
</view>
</uni-popup>
</view>
</template>
<script>
import area from './area.js'
export default {
props: {
label: {
default: '标签'
},
filterable: {
default: false,
type: Boolean
},
treeArr: {
default: function() {
return area
},
type: Array
}
},
data() {
return {
//---
selctValue: '',
//
popupVisible: true,
title: 'picker-view',
filterpickeArr: [],
visible: true,
//搜索框---
search: '',
//滚动框的值
pickeValue: '',
// 省市县数据
provinces: [],
cities: [],
counties: [],
selectProvince: '',
selectCity: '',
selectCounty: '',
}
},
mounted() {
this.filterpickeArr = [...this.treeArr]
this.resetArr()
},
methods: {
//恢复 下拉数据/搜索数据/选中数据
resetArr() {
this.provinces = [...this.treeArr]
this.cities = [...this.provinces[0].children]
this.counties = [...this.cities[0].children]
this.selectProvince = this.provinces[0]
this.selectCity = this.cities[0]
this.selectCounty = this.counties[0]
//搜索项
this.search = ''
},
//表单项
open() {
// 通过组件定义的ref调用uni-popup方法 ,如果传入参数 type 属性将失效 ,仅支持 ['top','left','bottom','right','center']
this.$refs.popup.open('bottom')
this.resetArr()
},
//弹出框---
// 输入框
inputChange(val) {
this.filter(val)
},
//过滤
filter(val) {
const filteArr = this.treeArr.filter((item, index) => {
return item.indexOf(val) !== -1;
})
this.filterpickeArr = [...filteArr]
this.pickeValue = this.filterpickeArr[0]
},
//取消
cancle() {
this.$refs.popup.close()
},
//确认
confirm() {
this.selctValue = this.pickeValue
this.$refs.popup.close()
const selectData = {
selectProvince: this.selectProvince,
selectCity: this.selectCity,
selectCounty: this.selectCounty
}
let showValue = this.selectProvince.value + '/' + this.selectCity.value + '/' + this.selectCounty.value
//回显
this.selctValue = showValue
this.$emit('confirm', selectData)
},
// 滑动
bindChange: function(e) {
const val = e.detail.value
//联动-实现
this.linkArr(val)
},
//联动实现
linkArr(val = []) {
let provinceIndex = val[0]
let cityIndex = val[1]
let countyIndex = val[2]
if (provinceIndex || provinceIndex === 0) {
//说明第一列 改变了------
this.cities = (this.provinces[provinceIndex] || {}).children
this.counties = (this.cities[0] || {}).children
//默认选中的数据
this.selectProvince = this.provinces[provinceIndex]
this.selectCity = this.cities[0]
this.selectCounty = this.counties[0]
}
if (cityIndex || cityIndex === 0) {
//说明第二列 改变了------
this.counties = (this.cities[cityIndex] || {}).children
//默认选中的数据
this.selectCity = this.cities[cityIndex]
this.selectCounty = this.counties[0]
}
if (countyIndex || countyIndex === 0) {
//说明第三列 改变了-----
//默认选中的数据
this.selectCounty = this.counties[countyIndex]
}
},
}
}
</script>
<style lang="scss" scoped>
.demo-uni-col {
height: 36px;
}
.picker-view {
height: 50vh;
width: 100vw;
margin-top: 20rpx;
}
.center {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,67 @@
## 下拉选择器
> **组件名complex-picker**
下拉选择器 ,可搜索( 快速过滤下拉选项 ) 可清空
## 属性
##### label --- 字符串类型 , 选择器的标签
##### treeArr --- 数组类型 , 省市区树形数据, 默认已经集成了全国数据, 可以直接使用
```javascript
#例如
[
{
"code": "110000",
"value": "北京市",
"children":[]
}
]
```
## 事件
##### @confirm --- 弹出框中点击 确定 触发, 参数为选中的省市区对象
## 示例代码
```vue
<template>
<view class="container">
<complex-picker-region label="年份" @confirm='confirm' />
</view>
</template>
<script>
// import 配置 测试用 实际引入遵循easycom, 直接导入 即可使用组件
// import complexpickerregion from '@/uni_modules/complex-plugin/complex-picker-region/components/complex-picker-region/complex-picker-region.vue'
export default {
// components: {
// complexpickerregion
// },
data() {
return {
}
},
methods: {
confirm(selectData) {
console.log('selectData', selectData);
}
}
}
</script>
<style>
.container {
padding: 20px;
font-size: 14px;
line-height: 24px;
}
</style>
```

View File

@@ -0,0 +1,102 @@
<template>
<view class="types">
<block v-for="(item,index) in types" :key="index">
<block v-if="values.includes(item.dictValue)">
<view :class="getStatusClass(item.dictValue)">
<text class="status-text">{{item.dictLabel}}</text>
</view>
</block>
</block>
</view>
</template>
<script>
export default {
name: "dict-type",
props: ['types', 'type'],
computed: {
values() {
if (this.type !== null && typeof this.type !== 'undefined') {
return Array.isArray(this.type) ? this.type : [String(this.type)];
} else {
return [];
}
},
},
data() {
return {
};
},
methods: {
getStatusClass(dictValue) {
switch (dictValue) {
case '1':
return 'status primary';
case '0':
case '3':
case '6':
case '2':
return 'status success';
case '10':
case '13':
return 'status warn';
case '14':
case '12':
return 'status gray';
default:
return 'status refuse';
}
},
}
}
</script>
<style scoped lang="scss">
.status {
position: relative;
padding:5px 10px;
border-radius: 0 8px 0 8px;
&.primary {
position: absolute;
top:0px;
right:0px;
background-color: #E8F4FF;
color: #1890FF;
border: 1px solid #1890FF;
}
&.success {
position: absolute;
top:0px;
right:0px;
background-color: #E7FAF0;
color: #71E2A3;
border: 1px solid #71E2A3;
}
&.gray {
position: absolute;
top:0px;
right:0px;
background-color: #f4f4f5;
color: #909399;
border: 1px solid #e9e9eb;
}
&.warn {
position: absolute;
top:0px;
right:0px;
background-color: #FFF8E6;
color: #FFBA00;
border: 1px solid #FFBA00;
}
&.refuse {
position: absolute;
top:0px;
right:0px;
background-color: #FFEDED;
color: #FF9292;
border: 1px solid #FFDBDB;
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<view>
<block v-for="(item,index) in status" :key="index">
<text v-if="values.includes(item.dictValue)">{{item.dictLabel}}</text>
</block>
</view>
</template>
<script>
export default {
name: "dict-status",
props: ['status', 'value'],
computed: {
values() {
if (this.value !== null && typeof this.value !== 'undefined') {
return Array.isArray(this.value) ? this.value : [String(this.value)];
} else {
return [];
}
},
},
data() {
return {
};
}
}
</script>
<style scoped>
text {
color: red;
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<view class="types">
<block v-for="(item,index) in types" :key="index">
<block v-if="values.includes(item.dictValue)">
<view :class="getStatusClass(item.dictValue)">
<text class="status-text">{{item.dictLabel}}</text>
</view>
</block>
</block>
</view>
</template>
<script>
export default {
name: "dict-type",
props: ['types', 'type'],
computed: {
values() {
if (this.type !== null && typeof this.type !== 'undefined') {
return Array.isArray(this.type) ? this.type : [String(this.type)];
} else {
return [];
}
},
},
data() {
return {
};
},
methods: {
getStatusClass(dictValue) {
switch (dictValue) {
case '0':
return 'status primary';
case '1':
return 'status success';
case '6':
return 'status success';
case '11':
return 'status refuse';
case '14':
return 'status gray';
default:
return 'status warn';
}
},
}
}
</script>
<style scoped lang="scss">
.status {
position: relative;
padding: 5px 10px;
border-radius: 0 8px 0 8px;
position: absolute;
top: 0px;
right: 0px;
&.primary {
background-color: #E8F4FF;
color: #1890FF;
border: 1px solid #1890FF;
}
&.success {
background-color: #E7FAF0;
color: #71E2A3;
border: 1px solid #71E2A3;
}
&.warn {
background-color: #FFF8E6;
color: #FFBA00;
border: 1px solid #FFBA00;
}
&.refuse {
background-color: #FFEDED;
color: #FF9292;
border: 1px solid #FFDBDB;
}
&.gray {
background-color: #f4f4f5;
border: 1px solid #e9e9eb;
color: #909399;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<view class="steps">
<view class="step" v-for="step in stepList" :key="step.taskId">
<image v-if="step.finishTime" class="icon" src="../../static/success.png" mode="widthFix"></image>
<image style="width:34rpx;" v-else class="icon" src="../../static/wating.png" mode="widthFix">
</image>
<image class="line" src="../../static/step-line.png" mode="widthFix"></image>
<view class="right">
<text>{{step.taskName}}:</text>
<text>{{step.assigneeName}}</text>
</view>
</view>
<view class="loading-more-top" v-if="stepList.length==0">
<uni-load-more status="loading" />
</view>
</view>
</template>
<script>
import {
flowRecord
} from "@/api/flowRecord/flowRecord.js";
export default {
name: "flow-step",
props:["procInsId","deployId"],
data() {
return {
stepList: [], //流程进度列表
};
},
created() {
console.log(this.procInsId);
console.log(this.deployId);
this.getFlowRecord();
},
methods:{
async getFlowRecord() {
let res = await flowRecord({
procInsId: this.procInsId,
deployId: this.deployId
});
this.stepList = res.data.flowList.reverse();
},
}
}
</script>
<style lang="scss" scoped>
.steps {
margin-top: 20px;
.step {
display: flex;
align-items: center;
margin-bottom: 50rpx;
.icon {
width: 35rpx;
}
.line {
flex: 0.7;
margin: 0 10px;
}
.right {
flex: 1.4;
text:first-child {
color: #9E9E9E;
margin-right: 10rpx;
}
}
}
}
</style>

55
components/navs/navs.vue Normal file
View File

@@ -0,0 +1,55 @@
<template>
<view class="navs">
<text @tap="navChange(item.val)" :class="current==item.val?'active':''" v-for="(item,index) in navs" :key="index">{{item.text}}</text>
</view>
</template>
<script>
export default {
props:['navs','loading'],
data() {
return {
current:"",
}
},
methods: {
navChange(val){
if(this.loading==undefined){
this.current=val;
this.$emit("change",val);
}else{
if(!this.loading){
this.current=val;
this.$emit("change",val);
}
}
}
}
}
</script>
<style scoped lang="scss">
.navs{
background-color: white;
padding:26rpx 40rpx;
position: fixed;
top: 44px;
left: 0;
right: 0;
z-index: 2;
box-shadow:0 0 5px #dadada;
text{
color: #202020;
opacity: 0.5;
margin-right: 40rpx;
padding-bottom:3rpx;
&.active{
opacity: 1;
border-bottom:3px solid #1890FF;
}
}
}
</style>

View File

@@ -0,0 +1,176 @@
<template>
<view class="u-waterfall">
<view id="u-left-column" class="u-column"><slot name="left" :leftList="leftList"></slot></view>
<view id="u-right-column" class="u-column"><slot name="right" :rightList="rightList"></slot></view>
</view>
</template>
<script>
/**
* waterfall 瀑布流
* @description 这是一个瀑布流形式的组件内容分为左右两列结合uView的懒加载组件效果更佳。相较于某些只是奇偶数左右分别或者没有利用vue作用域插槽的做法uView的瀑布流实现了真正的 组件化搭配LazyLoad 懒加载和loadMore 加载更多组件,让您开箱即用,眼前一亮。
* @tutorial https://www.uviewui.com/components/waterfall.html
* @property {Array} flow-list 用于渲染的数据
* @property {String Number} add-time 单条数据添加到队列的时间间隔单位ms见上方注意事项说明默认200
* @example <u-waterfall :flowList="flowList"></u-waterfall>
*/
export default {
name: "u-waterfall",
props: {
value: {
// 瀑布流数据
type: Array,
required: true,
default: function() {
return [];
}
},
// 每次向结构插入数据的时间间隔,间隔越长,越能保证两列高度相近,但是对用户体验越不好
// 单位ms
addTime: {
type: [Number, String],
default: 200
},
// id值用于清除某一条数据时根据此idKey名称找到并移除如数据为{idx: 22, name: 'lisa'}
// 那么该把idKey设置为idx
idKey: {
type: String,
default: 'id'
}
},
data() {
return {
leftList: [],
rightList: [],
tempList: [],
children: []
}
},
watch: {
copyFlowList(nVal, oVal) {
// 取差值,即这一次数组变化新增的部分
let startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
// 拼接上原有数据
this.tempList = this.tempList.concat(this.cloneData(nVal.slice(startIndex)));
this.splitData();
}
},
mounted() {
this.tempList = this.cloneData(this.copyFlowList);
this.splitData();
},
computed: {
// 破坏flowList变量的引用否则watch的结果新旧值是一样的
copyFlowList() {
return this.cloneData(this.value);
}
},
methods: {
async splitData() {
if (!this.tempList.length) return;
let leftRect = await this.$uGetRect('#u-left-column');
let rightRect = await this.$uGetRect('#u-right-column');
// 如果左边小于或等于右边,就添加到左边,否则添加到右边
let item = this.tempList[0];
// 解决多次快速上拉后可能数据会乱的问题因为经过上面的两个await节点查询阻塞一定时间加上后面的定时器干扰
// 数组可能变成[]导致此item值可能为undefined
if(!item) return ;
if (leftRect.height < rightRect.height) {
this.leftList.push(item);
} else if (leftRect.height > rightRect.height) {
this.rightList.push(item);
} else {
// 这里是为了保证第一和第二张添加时,左右都能有内容
// 因为添加第一张实际队列的高度可能还是0这时需要根据队列元素长度判断下一个该放哪边
if (this.leftList.length <= this.rightList.length) {
this.leftList.push(item);
} else {
this.rightList.push(item);
}
}
// 移除临时列表的第一项
this.tempList.splice(0, 1);
// 如果临时数组还有数据,继续循环
if (this.tempList.length) {
setTimeout(() => {
this.splitData();
}, this.addTime)
}
},
// 复制而不是引用对象和数组
cloneData(data) {
return JSON.parse(JSON.stringify(data));
},
// 清空数据列表
clear() {
this.leftList = [];
this.rightList = [];
// 同时清除父组件列表中的数据
this.$emit('input', []);
this.tempList = [];
},
// 清除某一条指定的数据根据id实现
remove(id) {
// 如果findIndex找不到合适的条件就会返回-1
let index = -1;
index = this.leftList.findIndex(val => val[this.idKey] == id);
if(index != -1) {
// 如果index不等于-1说明已经找到了要找的id根据index索引删除这一条数据
this.leftList.splice(index, 1);
} else {
// 同理于上方面的方法
index = this.rightList.findIndex(val => val[this.idKey] == id);
if(index != -1) this.rightList.splice(index, 1);
}
// 同时清除父组件的数据中的对应id的条目
index = this.value.findIndex(val => val[this.idKey] == id);
if(index != -1) this.$emit('input', this.value.splice(index, 1));
},
// 修改某条数据的某个属性
modify(id, key, value) {
// 如果findIndex找不到合适的条件就会返回-1
let index = -1;
index = this.leftList.findIndex(val => val[this.idKey] == id);
if(index != -1) {
// 如果index不等于-1说明已经找到了要找的id修改对应key的值
this.leftList[index][key] = value;
} else {
// 同理于上方面的方法
index = this.rightList.findIndex(val => val[this.idKey] == id);
if(index != -1) this.rightList[index][key] = value;
}
// 修改父组件的数据中的对应id的条目
index = this.value.findIndex(val => val[this.idKey] == id);
if(index != -1) {
// 首先复制一份value的数据
let data = this.cloneData(this.value);
// 修改对应索引的key属性的值为value
data[index][key] = value;
// 修改父组件通过v-model绑定的变量的值
this.$emit('input', data);
}
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-waterfall {
@include vue-flex;
flex-direction: row;
align-items: flex-start;
}
.u-column {
@include vue-flex;
flex: 1;
flex-direction: column;
height: auto;
}
.u-image {
width: 100%;
}
</style>

View File

View File

@@ -0,0 +1,89 @@
<template>
<view v-show="ishide" @touchmove.stop.prevent>
<!-- 遮罩 -->
<view class="mask" :style="maskStyle"></view>
<!-- 内容 -->
<view class="container" :style="containerStyle">
<view class="tip" :style="tipStyle">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
// 控制弹窗显隐
ishide: {
type: Boolean,
required: true
},
// 设置弹窗层级
zindex: {
type: Number,
default: 99
},
// 设置遮罩透明度
opacity: {
type: Number,
default: 0.6
},
// 设置内容区宽度
width: {
type: String,
default: '650rpx'
},
// 设置内容区高度
height: {
type: String,
default: '950rpx'
},
// 设置内容区圆角
radius: {
type: String
},
// 设置内容区底色
bgcolor: {
type: String,
default: '#FFFFFF'
}
},
computed: {
// 遮罩样式
maskStyle() {
return `z-index:${this.zindex}; background:rgba(0,0,0,${this.opacity});`
},
// 内容样式
tipStyle() {
return `
width:${this.width};
height:${this.height};
border-radius:${this.radius};
background-color:${this.bgcolor};
`
},
// 内容区域样式
containerStyle() {
return `position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index:${this.zindex + 1};`
}
}
}
</script>
<style scoped lang="scss">
.mask {
position: fixed;
bottom: 0;
right: 0;
left: 0;
top: 0;
}
.container {
position: fixed;
}
.tip {
margin-bottom: 30px;
/* 留出关闭按钮的空间 */
}
</style>