初始上传

This commit is contained in:
2026-04-04 17:27:12 +08:00
parent 4d80d28eb4
commit b7e11774ee
11191 changed files with 1588469 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
<template>
<unipopup ref="couponCategoryPop" type="center">
<view class="coupon-category-pop">
<view class="header flex justify-between">
<view class="title">选择分类</view>
<!-- <view class="pop-header-close" @click="$refs.couponCategoryPop.close()">
<text class="iconguanbi1 iconfont"></text>
</view> -->
</view>
<!-- :checkStrictly="true" -->
<view :overflow-y="true" class="body">
<scroll-view class="tree" :overflow-y="true">
<DaTreeVue2
ref="DaTreeRef"
:data="treeData"
labelField="category_name"
valueField="category_id"
childrenField="child_list"
:themeColors="'var(--primary-color)'"
expandChecked
showCheckbox
:defaultCheckedKeys="defaultCheckedKeysValue"
@change="handleTreeChange"></DaTreeVue2>
</scroll-view>
</view>
<view class="footer flex justify-end">
<button type="default" class="confirm btn" @click="confirm">确认</button>
<button type="default" class="btn" @click="$refs.couponCategoryPop.close()">取消</button>
</view>
</view>
</unipopup>
</template>
<script>
import DaTreeVue2 from '@/components/da-tree-vue2/index.vue'
import unipopup from '@/components/uni-popup/uni-popup.vue';
import index from './index.js';
export default {
components: {
DaTreeVue2,
unipopup,
},
mixins: [index]
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@@ -0,0 +1,87 @@
import {
getGoodsCategory
} from '@/api/goods.js'
export default {
name: 'couponCategoryPopup',
data() {
return {
treeData: [],
defaultCheckedKeysValue: [],
checkList: [], //选中以及半选中数据
}
},
mounted() {
this.getGoodsCategoryFn()
},
methods: {
getGoodsCategoryFn() {
getGoodsCategory({
level: 3
}).then(res => {
this.treeData = res.data
})
},
open(value) {
this.defaultCheckedKeysValue = this.$util.deepClone(value)
this.$refs.couponCategoryPop.open()
},
handleTreeChange(val) {
this.defaultCheckedKeysValue = this.$util.deepClone(val)
let halfCheckList = this.$refs.DaTreeRef.getHalfCheckedKeys()||[]
this.checkList = this.$util.deepClone(val.concat(halfCheckList))
},
//处理数据
getSelectedIdsAndNames(tree_selected, tree_all) {
let name_arr = [];
let id_arr = [];
let selected_num = 0;
for (let i in tree_selected) {
let item_selected = tree_selected[i];
let item_all = null;
tree_all.forEach((item) => {
if (item.category_id === item_selected.category_id) {
item_all = item;
return;
}
})
if (!item_all) throw '对比数据有误';
let title = item_selected.category_name;
id_arr.push(item_selected.category_id);
if (item_selected.child_num > 0) {
let res = this.getSelectedIdsAndNames(item_selected.child_list, item_all.child_list);
if (res.selected_num == item_all.child_num) {
selected_num++;
} else {
title += '' + res.name_arr.join('、') + '';
}
id_arr = id_arr.concat(res.id_arr);
} else {
selected_num++;
}
name_arr.push(title);
}
return {
selected_num: selected_num,
name_arr: name_arr,
id_arr: id_arr,
};
},
confirm() {
if(!this.checkList.length){
this.$util.showToast({
title: "请选择商品分类"
});
return false
}
getGoodsCategory({
level: 3,
category_ids:this.checkList.join(',')
}).then(res => {
let selectedData = this.getSelectedIdsAndNames(res.data, this.treeData);
this.$emit('confirm',selectedData)
this.$refs.couponCategoryPop.close()
})
}
}
}

View File

@@ -0,0 +1,42 @@
.coupon-category-pop{
width: 7rem;
background-color: #fff;
border-radius: 0.06rem;
.header{
padding: 0.15rem 0.2rem;
font-size: 0.14rem;
border-bottom: 0.01rem solid #e6e6e6;
}
.body{
padding: 0.2rem 0.3rem;
padding-bottom: 0.15rem;
box-sizing: border-box;
height: 4rem;
.tree{
height: 100%;
}
}
.footer{
padding: 0 0.3rem;
padding-bottom: 0.15rem;
.btn {
margin: 0;
display: inline-block;
padding: 0 0.2rem;
height: 0.36rem;
line-height: .36rem;
font-size: 0.14rem;
border-radius: 3px;
&.confirm{
background-color: var(--primary-color);
color: #fff;
margin-right: 0.15rem;
&::after{
border-width: 0;
}
}
}
}
}

View File

@@ -0,0 +1,110 @@
# 1.4.1
## 版本调整
建议更新,但需要注意,异步数据的时候,后台需返回 leaf 字段来判断是否末项数据
1. **调整数据项格式,新增 `leaf` 字段,来判断是否为末节点**
2. **调整数据项格式,新增 `sort` 字段,来排序节点位置**
3. **注意:异步加载数据,当为末项的时候,需要服务端数据返回 `leaf` 字段**
4. 新增 `alwaysFirstLoad` ,即异步数据总会在第一次展开节点时,拉取一次后台数据,来比对是否一致
5. 拆分 `field` 属性,**注意: 1.5.0 版本后将移除 `field` 属性**
6. 新增 `labelField``field.label`,指定节点对象中某个属性为**标签**字段,默认`label`
7. 新增 `valueField``field.key`,指定节点对象中某个属性为**值**字段,默认`value`
8. 新增 `childrenField``field.children`,指定节点对象中某个属性为**子树节点**字段,默认`children`
9. 新增 `disabledField``field.disabled`,指定节点对象中某个属性为**禁用**字段,默认`disabled`
10. 新增 `appendField``field.append`,指定节点对象中某个属性为**副标签**字段,默认`append`
11. 新增 `leafField``field.label`,指定节点对象中某个属性为**末级节点**字段,默认`leaf`
12. 新增 `sortField``field.label`,指定节点对象中某个属性为**排序**字段,默认`sort`
13. 新增 `isLeafFn` ,用来自定义控制数据项的末项
14. 更多的项目示例
15. 支持单选取消选中
16. 修复节点展开时可能存在的 bug
17. 修复节点选择可能存在的 bug
18. 调整为子节点默认继承父节点禁用属性
19. `setExpandedKeys` 添加参数一为 `all` 即可支持一键展开/收起全部节点
20. 其它更多优化
# 1.3.4
优化
1. 优化图标字体命名
# 1.3.3
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.3.2
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.3.1.1
修复
1. 修复 APP 兼容性引起的报错
# 1.3.1
## 建议更新
### 1.2.2~1.3.1 更新预览
1. 新增支持主题换色
2. 新增支持点击标签也能选中节点
3. 新增`field`字段 `append` 用于在标签后面显示小提示
4. 方法`setExpandedKeys`支持加载动态数据
5. 支持单选的`onlyRadioLeaf``true`时可点父节点展开/收起
6. 新增 `expandChecked`,控制选择时是否展开当前已选的所有下级节点
7. 新增 `checkedDisabled`,支持渲染禁用值
8. 新增 `packDisabledkey`,支持返回已选中的禁用的 key
9. 更多细节修复、优化请移步 Vue3 版的更新日志
后续版本仍不会实时同步 Vue3 版本,如急需新功能,请移步 Vue3 版
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.2.2
## 建议更新,优化诸多问题
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.2.1
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.2.0.1
优化
1. 优化小程序兼容
# 1.2.0
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.1.1.1
修复
1. 修复同步版本的错误写法引起的报错
# 1.1.1
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.1.0
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.0.6
新增
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384&update_log)
# 1.0.5
版本同步于 Vue3 版,[查看 Vue3 版更新日志](https://ext.dcloud.net.cn/plugin?id=12384),基于 Vue2 进行开发,支持单选、多选,全平台兼容。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,183 @@
// @ts-nocheck
export default {
/**
* 树的数据
*/
data: {
type: Array,
default: () => [],
},
/**
* 主题色
*/
themeColors: {
type: String,
default: '#007aff',
},
/**
* 是否开启多选,默认单选
*/
showCheckbox: {
type: Boolean,
default: false,
},
/**
* 默认选中的节点注意单选时为单个key多选时为key的数组
*/
defaultCheckedKeys: {
type: [Array, String, Number],
default: null,
},
/**
* 选择框的位置,可选 left/right
*/
checkboxPlacement: {
type: String,
default: 'left',
},
/**
* 是否默认展开全部
*/
defaultExpandAll: {
type: Boolean,
default: false,
},
/**
* 默认展开的节点
*/
defaultExpandedKeys: {
type: Array,
default: null,
},
/**
* 是否自动展开到选中的节点,默认不展开
*/
expandChecked: {
type: Boolean,
default: false,
},
/**
* 子项缩进距离默认40单位rpx
*/
indent: {
type: Number,
default: 0.4,
},
/**
* (旧)字段对应内容,默认为 {label: 'label',key: 'key', children: 'children', disabled: 'disabled', append: 'append'}
* 注意1.5.0版本后不再兼容
*/
field: {
type: Object,
default: null,
},
/**
* 标签字段(新,拆分了)
*/
labelField: {
type: String,
default: 'label',
},
/**
* 值字段(新,拆分了)
*/
valueField: {
type: String,
default: 'value',
},
/**
* 下级字段(新,拆分了)
*/
childrenField: {
type: String,
default: 'children',
},
/**
* 禁用字段(新,拆分了)
*/
disabledField: {
type: String,
default: 'disabled',
},
/**
* 末级节点字段(新,拆分了)
*/
leafField: {
type: String,
default: 'leaf',
},
/**
* 副标签字段(新,拆分了)
*/
appendField: {
type: String,
default: 'append',
},
/**
* 排序字段(新,拆分了)
*/
sortField: {
type: String,
default: 'sort',
},
isLeafFn: {
type: Function,
default: null,
},
/**
* 是否显示单选图标,默认显示
*/
showRadioIcon: {
type: Boolean,
default: true,
},
/**
* 单选时只允许选中末级,默认可随意选中
*/
onlyRadioLeaf: {
type: Boolean,
default: false,
},
/**
* 多选时,是否执行父子不关联的任意勾选,默认父子关联
*/
checkStrictly: {
type: Boolean,
default: false,
},
/**
* 为 true 时,空的 children 数组会显示展开图标
*/
loadMode: {
type: Boolean,
default: false,
},
/**
* 异步加载接口
*/
loadApi: {
type: Function,
default: null,
},
/**
* 是否总在首次的时候加载一下内容,来比对是否一致
*/
alwaysFirstLoad: {
type: Boolean,
default: false,
},
/**
* 是否渲染(操作)禁用值
*/
checkedDisabled: {
type: Boolean,
default: false,
},
/**
* 是否返回已禁用的但已选中的key
*/
packDisabledkey: {
type: Boolean,
default: true,
},
}

View File

@@ -0,0 +1,303 @@
# da-tree-vue2
一个基于 Vue2 的 tree(树)组件,同时支持主题换色,可能是最适合你的 tree(树)组件
`内容同步于 Vue3 版本,在此查看 ===>` **[Vue3 版](https://ext.dcloud.net.cn/plugin?id=12384)**
_与 Vue3 版本版本不同的是,此版本兼容更全面,比如 360 小程序、快应用等均支持_
### 关于使用
可在右侧的`使用 HBuilderX 导入插件``下载示例项目ZIP`,方便快速上手。
可通过下方的示例及文档说明,进一步了解使用组件相关细节参数。
插件地址https://ext.dcloud.net.cn/plugin?id=12692
### 组件示例
```jsx
<template>
<view>
<view>多选</view>
<view><button @click="doCheckedTree(['2'],true)">全选</button></view>
<view><button @click="doCheckedTree(['2'],false)">取消全选</button></view>
<view><button @click="doCheckedTree(['211','222'],true)">选中指定节点</button></view>
<view><button @click="doCheckedTree(['211','222'],false)">取消选中指定节点</button></view>
<view><button @click="doExpandTree('all',true)">展开全部节点</button></view>
<view><button @click="doExpandTree('all',false)">收起全部节点</button></view>
<view><button @click="doExpandTree(['22','23'],true)">展开节点</button></view>
<view><button @click="doExpandTree(['22','23'],false)">收起节点</button></view>
<DaTreeVue2
ref="DaTreeRef"
:data="roomTreeData"
labelField="name"
valueField="id"
defaultExpandAll
showCheckbox
:defaultCheckedKeys="defaultCheckedKeysValue"
@change="handleTreeChange"
@expand="handleExpandChange" />
<view>单选</view>
<DaTreeVue2
:data="roomTreeData"
labelField="name"
valueField="id"
defaultExpandAll
:defaultCheckedKeys="defaultCheckedKeysValue2"
@change="handleTreeChange"
@expand="handleExpandChange" />
<view>默认展开指定节点</view>
<DaTreeVue2
:data="roomTreeData"
labelField="name"
valueField="id"
showCheckbox
:defaultExpandedKeys="defaultExpandKeysValue3"
@change="handleTreeChange"
@expand="handleExpandChange" />
<view>异步加载数据</view>
<DaTreeVue2
:data="roomTreeData"
labelField="name"
valueField="id"
showCheckbox
loadMode
:loadApi="GetApiData"
defaultExpandAll
@change="handleTreeChange"
@expand="handleExpandChange" />
</view>
</template>
```
```js
/**
* 模拟创建一个接口数据
*/
function GetApiData(currentNode) {
const { key } = currentNode
return new Promise((resolve) => {
setTimeout(() => {
// 模拟返回空数据
if (key.indexOf('-') > -1) {
return resolve(null)
// return resolve([])
}
return resolve([
{
id: `${key}-1`,
name: `行政部X${key}-1`,
},
{
id: `${key}-2`,
name: `财务部X${key}-2`,
append: '定义了末项数据',
leaf: true,
},
{
id: `${key}-3`,
name: `资源部X${key}-3`,
},
{
id: `${key}-4`,
name: `资源部X${key}-3`,
append: '被禁用,无展开图标',
disabled: true,
},
])
}, 2000)
})
}
import DaTreeVue2 from '@/components/da-tree-vue2/index.vue'
export default {
components: { DaTreeVue2 },
data() {
return {
GetApiData,
// key的类型必须对应树数据key的类型
defaultCheckedKeysValue: ['211', '222'],
defaultCheckedKeysValue2: '222',
defaultExpandKeysValue3: ['212', '231'],
roomTreeData: [
{
id: '2',
name: '行政中心',
children: [
{
id: '21',
name: '行政部',
children: [
{
id: '211',
name: '行政一部',
children: null,
},
{
id: '212',
name: '行政二部',
children: [],
disabled: true,
},
],
},
{
id: '22',
name: '财务部',
children: [
{
id: '221',
name: '财务一部',
children: [],
disabled: true,
},
{
id: '222',
name: '财务二部',
children: [],
},
],
},
{
id: '23',
name: '人力资源部',
children: [
{
id: '231',
name: '人力一部',
children: [],
},
{
id: '232',
name: '人力二部',
append: '更多示例,请下载示例项目查看',
},
],
},
],
},
],
}
},
methods: {
doExpandTree(keys, expand) {
this.$refs.DaTreeRef?.setExpandedKeys(keys, expand)
const gek = this.$refs.DaTreeRef?.getExpandedKeys()
console.log('当前已展开的KEY ==>', gek)
},
doCheckedTree(keys, checked) {
this.$refs.DaTreeRef?.setCheckedKeys(keys, checked)
const gek = this.$refs.DaTreeRef?.getCheckedKeys()
console.log('当前已选中的KEY ==>', gek)
},
handleTreeChange(allSelectedKeys, currentItem) {
console.log('handleTreeChange ==>', allSelectedKeys, currentItem)
},
handleExpandChange(expand, currentItem) {
console.log('handleExpandChange ==>', expand, currentItem)
},
},
}
```
** 更多示例请下载/导入示例项目 ZIP 查看 **
### 组件参数
| 属性 | 类型 | 默认值 | 必填 | 说明 |
| :------------------ | :------------------------------ | :--------- | :--- | :--------------------------------------------------------------------------- |
| data | `Array` | - | 是 | 树的数据 |
| themeColor | `String` | `#007aff` | 否 | 主题色,十六进制 |
| defaultCheckedKeys | `Array` \| `Number` \| `String` | - | 否 | 默认选中的节点,单选为单个 key多选为 key 的数组 |
| showCheckbox | `Boolean` | `false` | 否 | 是否开启多选,默认单选 |
| checkStrictly | `Boolean` | `false` | 否 | 多选时,是否执行父子不关联的任意勾选,默认父子关联 |
| showRadioIcon | `Boolean` | `true` | 否 | 是否显示单选图标,默认显示 |
| onlyRadioLeaf | `Boolean` | `true` | 否 | 单选时只允许选中末级,默认可随意选中 |
| defaultExpandAll | `Boolean` | `false` | 否 | 是否默认展开全部 |
| defaultExpandedKeys | `Array` | - | 否 | 默认展开的节点 |
| indent | `Number` | `40` | 否 | 子项缩进距离,单位 rpx |
| checkboxPlacement | `String` | `left` | 否 | 选择框的位置,可选 left/right |
| loadMode | `Boolean` | `false` | 否 | 为 true 时,空的 children 数组会显示展开图标 |
| loadApi | `Function` | - | 否 | 选择框的位置,可选 left/right |
| checkedDisabled | `Boolean` | `false` | 否 | 是否渲染禁用值,默认不渲染 |
| packDisabledkey | `Boolean` | `true` | 否 | 是否返回已禁用的但已选中的 key默认返回禁用已选值 |
| expandChecked | `Boolean` | `false` | 否 | 是否自动展开到选中的节点,默认不展开 |
| alwaysFirstLoad | `Boolean` | `false` | 否 | 是否总在首次的时候加载一下内容,默认不加载,否则只有展开末级节点才会加载数据 |
| isLeafFn | `Function` | - | 否 | 自定义函数返回来控制数据项的末项 |
| field | `Object` | - | 否 | 字段对应内容,格式参考下方(1.5.0 后移除,请用单独的字段匹配) |
| labelField | `String` | `label` | 否 | 指定节点对象中某个属性为标签字段,默认`label` |
| valueField | `String` | `value` | 否 | 指定节点对象中某个属性为值字段,默认`value` |
| childrenField | `String` | `children` | 否 | 指定节点对象中某个属性为子树节点字段,默认`children` |
| disabledField | `String` | `disabled` | 否 | 指定节点对象中某个属性为禁用字段,默认`disabled` |
| appendField | `String` | `append` | 否 | 指定节点对象中某个属性为副标签字段,默认`append` |
| leafField | `String` | `leaf` | 否 | 指定节点对象中某个属性为末级节点字段,默认`leaf` |
| sortField | `String` | `sort` | 否 | 指定节点对象中某个属性为排序字段,默认`sort` |
**field 格式(1.5.0 后移除,请用单独的字段匹配)**
```js
{
label: 'label',
key: 'key',
children: 'children',
disabled: 'disabled',
append: 'append'
}
```
### 组件事件
| 事件名称 | 回调参数 | 说明 |
| :------- | :-------------------------------------- | :-------------- |
| change | `(allCheckedKeys, currentItem) => void` | 选中时回调 |
| expand | `(expandState, currentItem) => void` | 展开/收起时回调 |
### 组件方法
| 方法名称 | 参数 | 说明 |
| :------------------ | :--------------- | :------------------------------------------------------------------------------------------------ |
| setCheckedKeys | `(keys,checked)` | 设置指定 key 的节点选中/取消选中的状态。注: keys 单选时为 key多选时为 key 的数组 |
| setExpandedKeys | `(keys,expand)` | 设置指定 key 的节点展开/收起的状态,当 keys 为 all 时即代表展开/收起全部。注keys 为数组或 `all` |
| getCheckedKeys | - | 返回已选的 key |
| getHalfCheckedKeys | - | 返回半选的 key |
| getUncheckedKeys | - | 返回未选的 key |
| getCheckedNodes | - | 返回已选的节点 |
| getUncheckedNodes | - | 返回未选的节点 |
| getHalfCheckedNodes | - | 返回半选的节点 |
| getExpandedKeys | - | 返回已展开的 key |
| getUnexpandedKeys | - | 返回未展开的 key |
| getExpandedNodes | - | 返回已展开的节点 |
| getUnexpandedNodes | - | 返回未展开的节点 |
### 组件版本
v1.4.1
### 差异化
已通过测试
> - H5 页面
> - 微信小程序
> - 支付宝、钉钉小程序
> - 字节跳动、抖音、今日头条小程序
> - 百度小程序
> - 飞书小程序
> - QQ 小程序
> - 京东小程序
> - 快应用
> - 360 小程序
未测试
> - 快手小程序由于非企业用户暂无演示
### 开发组
[@CRLANG](https://crlang.com)

View File

@@ -0,0 +1,151 @@
// @ts-nocheck
/** 未选 */
export const unCheckedStatus = 0
/** 半选 */
export const halfCheckedStatus = 1
/** 选中 */
export const isCheckedStatus = 2
/**
* 深拷贝内容
* @param originData 拷贝对象
* @author crlang(https://crlang.com)
*/
export function deepClone(originData) {
const type = Object.prototype.toString.call(originData)
let data
if (type === '[object Array]') {
data = []
for (let i = 0; i < originData.length; i++) {
data.push(deepClone(originData[i]))
}
} else if (type === '[object Object]') {
data = {}
for (const prop in originData) {
// eslint-disable-next-line no-prototype-builtins
if (originData.hasOwnProperty(prop)) { // 非继承属性
data[prop] = deepClone(originData[prop])
}
}
} else {
data = originData
}
return data
}
/**
* 获取所有指定的节点
* @param type
* @param value
* @author crlang(https://crlang.com)
*/
export function getAllNodes(list, type, value, packDisabledkey = true) {
if (!list || list.length === 0) {
return []
}
const res = []
for (let i = 0; i < list.length; i++) {
const item = list[i]
if (item[type] === value) {
if ((packDisabledkey && item.disabled) || !item.disabled) {
res.push(item)
}
}
}
return res
}
/**
* 获取所有指定的key值
* @param type
* @param value
* @author crlang(https://crlang.com)
*/
export function getAllNodeKeys(list, type, value, packDisabledkey = true) {
if (!list || list.length === 0) {
return null
}
const res = []
for (let i = 0; i < list.length; i++) {
const item = list[i]
if (item[type] === value) {
if ((packDisabledkey && item.disabled) || !item.disabled) {
res.push(item.key)
}
}
}
return res.length ? res : null
}
/**
* 错误输出
*
* @param msg
*/
export function logError(msg, ...args) {
console.error(`DaTree: ${msg}`, ...args)
}
const toString = Object.prototype.toString
export function is(val, type) {
return toString.call(val) === `[object ${type}]`
}
/**
* 是否对象(Object)
* @param val
*/
export function isObject(val) {
return val !== null && is(val, 'Object')
}
/**
* 是否数字(Number)
* @param val
*/
export function isNumber(val) {
return is(val, 'Number')
}
/**
* 是否字符串(String)
* @param val
*/
export function isString(val) {
return is(val, 'String')
}
/**
* 是否函数方法(Function)
* @param val
*/
export function isFunction(val) {
return typeof val === 'function'
}
/**
* 是否布尔(Boolean)
* @param val
*/
export function isBoolean(val) {
return is(val, 'Boolean')
}
/**
* 是否数组(Array)
* @param val
*/
export function isArray(val) {
return val && Array.isArray(val)
}

View File

@@ -0,0 +1,159 @@
import {getCardList} from '@/api/card.js';
import {mapGetters} from 'vuex';
export default {
name: 'nsCard',
props: {
type: {
type: String,
default: 'oncecard'
}
},
data() {
return {
goodsType: '',
pageSize: 35,
onceCardData: {
page: 0,
total: 1,
list: []
},
timeCardData: {
page: 0,
total: 1,
list: []
},
commonCardData: {
page: 0,
total: 1,
list: []
},
itemNum: 3,
mediaQueryOb: null,
selectCardSkuId: [],
isLoad: false
};
},
created() {
this.goodsType = this.type;
this.init();
},
computed: {
...mapGetters(['buyCardGoodsData'])
},
watch: {
buyCardGoodsData: {
// 每个属性值发生变化就会调用这个函数
handler(newVal, oldVal) {
this.selectCardSkuId = [];
if(!Object.values(this.buyCardGoodsData).length) return false;
Object.values(this.buyCardGoodsData).forEach((item,index)=>{
this.selectCardSkuId.push(item.sku_id);
});
},
// 深度监听 属性的变化
deep: true
}
},
mounted() {
this.mediaQueryOb = uni.createMediaQueryObserver(this);
this.mediaQueryOb.observe({maxWidth: 1500}, matches => {
if (matches) this.itemNum = 2;
});
this.mediaQueryOb.observe({minWidth: 1501, maxWidth: 1700}, matches => {
if (matches) this.itemNum = 3;
});
this.mediaQueryOb.observe({minWidth: 1701}, matches => {
if (matches) this.itemNum = 4;
});
},
destroyed() {
this.mediaQueryOb.disconnect();
},
methods: {
init() {
this.isLoad = false;
this.onceCardData.page = 0;
this.timeCardData.page = 0;
this.commonCardData.page = 0;
this.getOnceCard();
this.getTimeCard();
this.getCommonCard();
},
switchGoodsType(type) {
this.goodsType = type;
},
//卡项相关
goodsSelect(data) {
if (data.stock <= 0) return;
let _buyCardGoodsData = this.$util.deepClone(this.buyCardGoodsData);
if (_buyCardGoodsData['sku_' + data.sku_id]) {
_buyCardGoodsData['sku_' + data.sku_id].num += 1;
} else {
_buyCardGoodsData['sku_' + data.sku_id] = data;
_buyCardGoodsData['sku_' + data.sku_id].num = 1;
}
this.$store.commit('buycard/setGoodsData', _buyCardGoodsData);
this.$store.commit('buycard/setActive', 'SelectGoodsAfter');
},
getOnceCard() {
this.isLoad = false;
if (this.onceCardData.page + 1 > this.onceCardData.total) return;
this.onceCardData.page += 1;
getCardList({
page: this.onceCardData.page,
page_size: this.pageSize,
card_type: 'oncecard',
goods_state: 1,
status: 1,
}).then(res => {
if (res.code == 0) {
this.isLoad = true;
this.onceCardData.total = res.data.page_count || 1;
if (this.onceCardData.page == 1) this.onceCardData.list = [];
if (res.data.list.length) this.onceCardData.list = this.onceCardData.list.concat(res.data.list);
}
});
},
getTimeCard() {
if (this.timeCardData.page + 1 > this.timeCardData.total) return;
this.timeCardData.page += 1;
getCardList({
page: this.timeCardData.page,
card_type: 'timecard',
goods_state: 1,
page_size: this.pageSize,
status: 1,
}).then(res => {
if (res.code == 0) {
this.timeCardData.total = res.data.page_count || 1;
if (this.timeCardData.page == 1) this.timeCardData.list = [];
if (res.data.list.length) this.timeCardData.list = this.timeCardData.list.concat(
res.data.list);
}
});
},
getCommonCard() {
if (this.commonCardData.page + 1 > this.commonCardData.total) return;
this.commonCardData.page += 1;
getCardList({
page: this.commonCardData.page,
card_type: 'commoncard',
goods_state: 1,
page_size: this.pageSize,
status: 1,
}).then(res => {
if (res.code == 0) {
this.commonCardData.total = res.data.page_count || 1;
if (this.commonCardData.page == 1) this.commonCardData.list = [];
if (res.data.list.length) this.commonCardData.list = this.commonCardData.list.concat(res.data.list);
}
});
},
}
};

View File

@@ -0,0 +1,228 @@
.container {
height: 100%;
}
.header-action {
padding: 0.22rem 0.24rem;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
border-radius: 0.04rem;
.header-action-left {
display: flex;
align-items: center;
height: 0.44rem;
background-color: var(--primary-color-light-9);
border-radius: 0.22rem;
view {
min-width: 1.02rem;
height: 0.44rem;
line-height: 0.44rem;
text-align: center;
font-size: 0.14rem;
border-left-width: 0;
transition: all 0.3s;
cursor: pointer;
border-radius: 0.22rem;
color: $primary-color;
&.active {
color: #fff;
background-color: $primary-color;
}
&:first-child {
border-left-width: 0.01rem;
box-shadow: none;
}
}
}
}
.content {
margin-top: .2rem;
box-sizing: border-box;
height: calc(100% - 1.08rem);
transform: rotate(0);
display: flex;
flex-direction: column;
}
.list-wrap {
flex: 1;
height: 0;
}
.table-list {
display: flex;
align-items: center;
flex-wrap: wrap;
.table-item {
border: 0.01rem solid #fff;
box-sizing: border-box;
padding: 0.1rem 0.18rem 0.1rem 0.1rem;
background-color: #fff;
margin-bottom: 0.12rem;
margin-right: 0.12rem;
cursor: pointer;
transition: border-color, background-color 0.3s;
position: relative;
border-radius: 0.04rem;
&.item-mum-2{
width: calc((100% - 0.25rem) / 2);
}
&.item-mum-3{
width: calc((100% - 0.37rem) / 3);
}
&.item-mum-4{
width: calc((100% - 0.49rem) / 4);
}
.item-other {
display: flex;
flex-wrap: wrap;
margin-left: 0.1rem;
}
.item-img {
width: 0.9rem;
height: 0.9rem;
display: flex;
align-items: center;
overflow: hidden;
-ms-flex-negative: 0;
-webkit-flex-shrink: 0;
flex-shrink: 0;
image {
width: 100%;
border-radius: 0.03rem;
}
}
.item-name {
height: 0.4rem;
line-height: 0.2rem;
max-width: 1.58rem;
margin-bottom: 0.05rem;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.no-stock {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
cursor: not-allowed;
image{
height: 60%;
}
}
.item-info {
cursor: pointer;
flex: 1;
display: flex;
.item-time {
font-size: 0.12rem;
color: #909399;
}
.item-money {
font-size: 0.14rem;
color: $primary-color;
height: 0.19rem;
line-height: 0.19rem;
}
.item-stock {
height: 0.17rem;
font-size: 0.12rem;
color: #808695;
line-height: 0.17rem;
}
}
}
.table-item.yes-stock {
&:hover {
background-color: var(--primary-color-light-9);
border-color: $primary-color;
}
&.focus,
&:focus {
background-color: var(--primary-color-light-9);
border-color: $primary-color;
outline: none;
}
&.active {
border-color: $primary-color;
background-color: $primary-color;
color: #fff;
.item-time,
.item-money,
.item-stock {
color: #fff;
}
}
}
}
.empty {
text-align: center;
padding-top: 1.2rem;
image {
width: 2rem;
}
.tips {
color: #999;
margin-top: 0.15rem;
}
}
/deep/ .uni-scroll-view {
&::-webkit-scrollbar {
width: 0.06rem;
height: 0.06rem;
background-color: rgba($color: #000000, $alpha: 0);
}
&::-webkit-scrollbar-button {
display: none;
}
&::-webkit-scrollbar-thumb {
border-radius: 0.06rem;
box-shadow: inset 0 0 0.06rem rgba(45, 43, 43, 0.45);
background-color: #ddd;
display: none;
}
&:hover::-webkit-scrollbar-thumb {
display: block;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
}

View File

@@ -0,0 +1,111 @@
<template>
<view class="container">
<view class="header-action common-wrap">
<view class="header-action-left">
<view :class="{ active: goodsType == 'oncecard' }" @click="switchGoodsType('oncecard')">限次卡</view>
<view :class="{ active: goodsType == 'timecard' }" @click="switchGoodsType('timecard')">限时卡</view>
<view :class="{ active: goodsType == 'commoncard' }" @click="switchGoodsType('commoncard')">通用卡</view>
</view>
</view>
<view class="content">
<scroll-view scroll-y="true" class="list-wrap" @scrolltolower="getOncecard()" v-show="goodsType == 'oncecard'">
<view class="table-list" v-show="onceCardData.list.length > 0">
<view class="table-item" :class="{'yes-stock': item.stock>0, 'item-mum-2': itemNum == 2, 'item-mum-3': itemNum == 3, 'item-mum-4': itemNum == 4, 'active': selectCardSkuId.indexOf(item.sku_id) > -1 }" v-for="(item, index) in onceCardData.list" :key="index" @click="goodsSelect(item)">
<view class="item-info">
<view class="item-img">
<image v-if="item.goods_image == '@/static/goods/goods.png'" src="@/static/goods/goods.png" mode="widthFix"/>
<image v-else :src="$util.img(item.goods_image.split(',')[0], { size: 'small' })" @error="item.goods_image = '@/static/goods/goods.png'" mode="widthFix"/>
</view>
<view class="item-other flex-1">
<view class="item-name">{{ item.goods_name }}</view>
<view class="w-full self-end">
<view class="item-money">
<text class="util"></text>
{{ item.discount_price | moneyFormat }}
</view>
</view>
</view>
</view>
<view class="no-stock" v-if="item.stock <= 0">
<image src="@/static/stock/stock_empty.png" mode="heightFix"/>
</view>
</view>
</view>
<view class="empty" v-if="isLoad && !onceCardData.list.length">
<image src="@/static/goods/goods_empty.png" mode="widthFix"/>
<view class="tips">暂无卡项</view>
</view>
</scroll-view>
<scroll-view scroll-y="true" class="list-wrap" @scrolltolower="getTimecard()" v-show="goodsType == 'timecard'">
<view class="table-list" v-show="timeCardData.list.length > 0">
<view class="table-item" :class="{'yes-stock': item.stock>0, 'item-mum-2': itemNum == 2, 'item-mum-3': itemNum == 3, 'item-mum-4': itemNum == 4,}" v-for="(item, index) in timeCardData.list" :key="index" @click="goodsSelect(item)">
<view class="item-info">
<view class="item-img">
<image v-if="item.goods_image == '@/static/goods/goods.png'" src="@/static/goods/goods.png" mode="widthFix"/>
<image v-else :src="$util.img(item.goods_image.split(',')[0], { size: 'small' })" @error="item.goods_image = '@/static/goods/goods.png'" mode="widthFix"/>
</view>
<view class="item-other flex-1">
<view class="item-name">{{ item.goods_name }}</view>
<view class="w-full self-end">
<view class="item-money">
<text class="util"></text>
{{ item.discount_price | moneyFormat }}
</view>
</view>
</view>
</view>
<view class="no-stock" v-if="item.stock <= 0">
<image src="@/static/stock/stock_empty.png" mode="heightFix"/>
</view>
</view>
</view>
<view class="empty" v-if="!timeCardData.list.length">
<image src="@/static/goods/goods_empty.png" mode="widthFix"/>
<view class="tips">暂无卡项</view>
</view>
</scroll-view>
<scroll-view scroll-y="true" class="list-wrap" @scrolltolower="getCommoncard()" v-show="goodsType == 'commoncard'">
<view class="table-list" v-show="commonCardData.list.length > 0">
<view class="table-item" :class="{'yes-stock': item.stock>0, 'item-mum-2': itemNum == 2, 'item-mum-3': itemNum == 3, 'item-mum-4': itemNum == 4,}" v-for="(item, index) in commonCardData.list" :key="index" @click="goodsSelect(item)">
<view class="item-info">
<view class="item-img">
<image v-if="item.goods_image == '@/static/goods/goods.png'" src="@/static/goods/goods.png" mode="widthFix"/>
<image v-else :src="$util.img(item.goods_image.split(',')[0], { size: 'small' })" @error="item.goods_image = '@/static/goods/goods.png'" mode="widthFix"/>
</view>
<view class="item-other flex-1">
<view class="item-name">{{ item.goods_name }}</view>
<view class="w-full self-end">
<view class="item-money">
<text class="util"></text>
{{ item.discount_price | moneyFormat }}
</view>
</view>
</view>
</view>
<view class="no-stock" v-if="item.stock <= 0">
<image src="@/static/stock/stock_empty.png" mode="heightFix"/>
</view>
</view>
</view>
<view class="empty" v-if="!commonCardData.list.length">
<image src="@/static/goods/goods_empty.png" mode="widthFix"/>
<view class="tips">暂无卡项</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import index from './index.js';
export default {
mixins: [index]
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@@ -0,0 +1,520 @@
<template>
<unipopup ref="dialogRef" type="center" :maskClick="false">
<view class="stock-dialog-wrap">
<view class="stock-dialog-head">
<text>商品选择</text>
<text class="iconfont iconguanbi1" @click="$emit('change', false)"></text>
</view>
<view class="stock-dialog-body">
<view class="tree">
<scroll-view scroll-y="true" class="list-wrap">
<view class="item" :class="{ 'active': option.category_id === '' }" @click="itemClick({ category_id: '', child_num: 0 })">
<view class="icon"></view>
<view>全部分类</view>
</view>
<view v-for="(item, key) in goodsCategoryList" :key="key">
<view class="item" :class="{ 'active': option.category_id === item.category_id }" @click="itemClick(item)">
<view class="icon" :class="{ 'active': activeList.indexOf(item.category_id) != -1 }">
<text v-if="item.child_num" class="iconfont iconsanjiao_xia"></text>
</view>
<view>{{ item.title }}</view>
</view>
<template v-if="item.child_num">
<view v-show="activeList.indexOf(item.category_id) != -1" v-for="(item2, key2) in item.children" :key="key2" class="level">
<view class="item" :class="{ 'active': option.category_id === item2.category_id }" @click="itemClick(item2)">
<view class="icon" :class="{ 'active': activeList.indexOf(item2.category_id) != -1 }">
<text v-if="item2.child_num" class="iconfont iconsanjiao_xia"></text>
</view>
<view>{{ item2.title }}</view>
</view>
<template>
<view v-show="activeList.indexOf(item2.category_id) != -1" v-for="(item3, key3) in item2.children" :key="key3" class="level">
<view class="item item2" @click="itemClick(item3)">
<view class="icon"></view>
<view>{{ item3.title }}</view>
</view>
</view>
</template>
</view>
</template>
</view>
</scroll-view>
</view>
<view class="stock-dialog-table">
<view class="search common-form">
<view class="common-form-item">
<view class="form-input-inline" v-if="isInstallSupply == 1">
<select-lay :zindex="10" :value="option.supplier_id" name="supplier_id" placeholder="请选择供应商" :options="supplierList" @selectitem="selectSupplier"/>
</view>
<view class="form-input-inline">
<select-lay :zindex="10" :value="option.brand_id" name="brand_id" placeholder="请选择品牌" :options="brandList" @selectitem="selectBrand"/>
</view>
<view class="form-input-inline">
<select-lay :zindex="10" :value="option.goods_class" name="goods_class" placeholder="请选择类型" :options="goodsClassList" @selectitem="selectGoodsClass"/>
</view>
<view class="form-inline">
<view class="form-input-inline">
<input type="text" v-model="option.search_text" @confirm="getStoreGoods" placeholder="请输入名称/编码" class="form-input" />
</view>
</view>
<view class="form-inline common-btn-wrap">
<button type="default" class="screen-btn" @click="getStoreGoods">筛选</button>
</view>
</view>
</view>
<uniDataTable class="goods-table" pk="sku_id" :url="url" :option="option" :cols="cols" :pagesize="8" ref="goodsListTable"></uniDataTable>
</view>
</view>
<view class="btn">
<button type="primary" class="primary-btn submit" @click="submit('close')">选中</button>
<button type="primary" class="default-btn" @click="$emit('change', false)">取消</button>
</view>
</view>
</unipopup>
</template>
<script>
import unipopup from '@/components/uni-popup/uni-popup.vue';
import uniDataTable from '@/components/uni-data-table/uni-data-table-new.vue';
import {getManageGoodsCategory,getGoodsSceen,getSkuListBySelect} from '@/api/goods.js';
export default {
name: 'stockDialog',
components: {
unipopup,
uniDataTable
},
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: Boolean,
default: false
},
params: {
type: Object,
default: ()=>{
return {}
}
},
goodsClass:{
type: Array,
default: ()=>{
return [1,4,5,6];
},
}
},
data() {
return {
goodsCategoryList: {},
activeList: [],//下拉激活
option: {
category_id: '',
search_text: '',
is_weigh: 0,
page_size: 8,
goods_class_all:'',
goods_class:'',
supplier_id:'',
brand_id:'',
},
checkList: {},
cols: [],
url: '',
goodsClassList: [],
isInstallSupply:0,
supplierList:[],
brandList:[],
}
},
watch: {
value: {
handler: function (val) {
if (val) {
this.$nextTick(() => {
this.option = Object.assign(this.option, this.params);
if (this.params.temp_store_id && this.params.temp_store_id == '') {
delete this.option.temp_store_id
}
this.$refs.dialogRef.open()
})
} else {
this.$nextTick(() => {
this.option = Object(this.option, {
category_id: '',
search_text: '',
is_weigh: 0,
page: 1,
page_size: 8,
});
this.checkList = {};
this.$refs.dialogRef.close()
})
}
},
immediate: true
},
},
mounted() {
this.skuConfig();
this.getGoodsCategory();
this.getScreen();
this.getGoodsClassList();
},
methods: {
skuConfig(){
this.cols = [{
width: 20,
align: 'center',
checkbox: true,
}, {
field: 'account_data',
width: 50,
title: '商品信息',
align: 'left',
templet: data => {
let img = this.$util.img(data.sku_image);
let html = `
<view class="goods-content">
<image class="goods-img" src="${img}" mode="aspectFit"/>
<text class="goods-name multi-hidden" title="${data.sku_name}">${data.sku_name}</text>
</view>
`;
return html;
}
}, {
field: 'stock',
width: 22,
title: '库存',
align: 'center',
templet: data => {
return (data.stock || 0);
}
}, {
width: 22,
title: '单位',
templet: data => {
return (data.unit || '件');
}
}];
this.url = '/cashier/storeapi/goods/getSkuListBySelect';
},
selectGoodsClass(index) {
this.option.goods_class = index == -1 ? '' : this.goodsClassList[index].value.toString();
this.getStoreGoods();
},
selectBrand(index){
this.option.brand_id = index == -1 ? '' : this.brandList[index].value.toString();
this.getStoreGoods();
},
selectSupplier(index){
this.option.supplier_id = index == -1 ? '' : this.supplierList[index].value.toString();
this.getStoreGoods();
},
getGoodsCategory() {
getManageGoodsCategory().then(res=>{
uni.hideLoading();
if (res.data && Object.keys(res.data)) {
this.goodsCategoryList = res.data
} else {
this.$util.showToast({
title: res.message
});
}
})
},
getScreen(){
getGoodsSceen().then(res=>{
this.isInstallSupply = res.data.is_install_supply;
(res.data.supplier_list || []).forEach((item) => {
this.supplierList.push({
value : item.supplier_id,
label : item.title,
})
})
res.data.brand_list.forEach((item) => {
this.brandList.push({
value : item.brand_id,
label : item.brand_name,
})
})
})
},
getGoodsClassList(){
let goodsClassList = [
{
value: this.$util.goodsClassDict.real,
label: '实物商品'
}, {
value: this.$util.goodsClassDict.service,
label: '服务项目'
}, {
value: this.$util.goodsClassDict.card,
label: '卡项套餐'
}, {
value: this.$util.goodsClassDict.weigh,
label: '称重商品'
}
];
let goods_class_all = [];
goodsClassList.forEach((item)=>{
if(this.goodsClass.indexOf(item.value) > -1){
this.goodsClassList.push(item);
goods_class_all.push(item.value);
}
})
this.option.goods_class_all = goods_class_all.toString();
},
itemClick(item) {//tree点击
this.option.category_id = item.category_id;
var index = this.activeList.indexOf(item.category_id);
if (item.child_num && index === -1) {
this.activeList.push(item.category_id);
} else if (item.child_num && index != -1) {
this.activeList.splice(index, 1);
}
this.$forceUpdate();
this.getStoreGoods();
},
getStoreGoods() {//表格查询
this.$refs.goodsListTable.load({
page: 1
});
},
submit(action) {
let res = this.$refs.goodsListTable.getSelectData();
if(res.selectedNum == 0){
this.$util.showToast({
title: '请选择商品'
});
return false
}
if(res.allSelected){
let option = this.$util.deepClone(this.option);
option.unselected_sku_ids = Object.keys(res.unselectedData).toString();
option.page_size = 0;
uni.showLoading({
title: '数据获取中'
});
getSkuListBySelect(option).then(res=>{
uni.hideLoading();
this.$emit('selectGoods', res.data.list);
this.$refs.goodsListTable.clearCheck();
if(action == 'close'){
this.$emit('change', false);
}
})
}else{
this.$emit('selectGoods', Object.values(res.selectedData));
this.$refs.goodsListTable.clearCheck();
if(action == 'close'){
this.$emit('change', false);
}
}
}
}
}
</script>
<style lang="scss" scoped>
.stock-dialog-wrap {
background-color: #fff;
border-radius: 0.05rem;
width: 100%;
height: 75vh;
.stock-dialog-head {
padding: 0 0.15rem;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.15rem;
height: 0.45rem;
border-bottom: 0.01rem solid #e8eaec;
.iconguanbi1 {
font-size: $uni-font-size-lg;
}
}
.stock-dialog-body {
width: 100%;
// height: 7.3rem;
height: calc(100% - 0.45rem - 0.58rem);
padding: 0.1rem 0.2rem 0 0.2rem;
box-sizing: border-box;
display: flex;
.tree {
width: 1.8rem;
// height: 7.1rem;
height: 100%;
overflow-y: auto;
border-right: 0.01rem solid #e8eaec;
flex-shrink: 0;
flex-basis: auto;
flex-grow: 0;
box-sizing: border-box;
.list-wrap {
width: 100%;
height: 100%;
>view {
box-sizing: border-box;
width: 100%;
}
view.item {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
line-height: 0.3rem;
min-height: 0.3rem;
font-weight: 500;
&.active {
.icon,
view {
color: $primary-color !important;
}
background-color: #f7f7f7;
}
&:hover {
background-color: #f7f7f7;
}
.icon {
width: 0.2rem;
height: 0.3rem;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(-90deg);
transition: all ease 0.5s;
&.active {
transform: rotate(-45deg);
}
}
}
.level {
width: 100%;
box-sizing: border-box;
.item {
padding-left: 0.2rem;
}
.item2 {
padding-left: 0.4rem;
}
}
}
}
.stock-dialog-table {
width: 6.6rem;
margin-left: 0.2rem;
display: flex;
flex-direction: column;
height: 100%;
.search {
display: flex;
justify-content: flex-end;
}
.goods-table /deep/{
flex: 1;
height: 0;
.tbody{
height: calc( 100% - 0.5rem - 0.5rem );
overflow-y: auto;
&::-webkit-scrollbar{
width: 0.06rem;
height: 0.06rem;
background-color: rgba(0, 0, 0, 0);
}
&::-webkit-scrollbar-button{
display: none;
}
&::-webkit-scrollbar-thumb{
border-radius: 0.06rem;
box-shadow: inset 0 0 0.06rem rgba(45, 43, 43, 0.45);
background-color: #ddd;
}
&::-webkit-scrollbar-track{
background-color: transparent;
}
}
}
}
}
.btn {
display: flex;
justify-content: flex-end;
border-top: 0.01rem solid #e8eaec;
padding: 0.1rem 0.2rem 0.1rem 0.2rem;
box-sizing: border-box;
height: 0.58rem;
.default-btn,
.primary-btn {
margin: 0;
}
.default-btn {
border: 0.01rem solid #e8eaec !important;
}
.submit{
margin-right: 0.15rem;
}
.default-btn::after {
display: none;
}
}
.common-form{
.common-btn-wrap {
margin-left: 0;
.screen-btn {
margin-right: 0;
padding-left:14px;
padding-right:14px;
}
}
.common-form-item {
margin-bottom: 0.1rem;
.form-input-inline {
width: 1.3rem;
}
}
}
/deep/ .goods-content {
display: flex;
.goods-img {
margin-right: 0.1rem;
width: 0.5rem;
height: 0.5rem;
flex-shrink: 0;
flex-basis: auto;
flex-grow: 0;
}
}
}
</style>

View File

@@ -0,0 +1,168 @@
<template>
<view class="ns-record">
<view class="title">
库存记录
<text class="iconfont iconguanbi1" @click="close"></text>
</view>
<view class="table">
<view class="table-th">
<view class="table-td" style="width: 15%;">规格名称</view>
<view class="table-td" style="width: 12%;">业务类型</view>
<view class="table-td" style="width: 14%;">原始数量</view>
<view class="table-td" style="width: 14%;">变动数量</view>
<view class="table-td" style="width: 14%;">剩余数量</view>
<view class="table-td" style="width: 14%;">入库单价</view>
<view class="table-td" style="width: 17%;">创建时间</view>
</view>
<scroll-view scroll-y="true" class="table-tb">
<view class="table-tr" v-for="(item, index) in list" :key="index">
<view class="table-td" style="width: 15%;">{{ item.spec_name ? item.spec_name : item.goods_name }}</view>
<view class="table-td" style="width: 12%;">{{ item.name }}</view>
<view class="table-td" style="width: 14%;">{{ item.before_store_stock }}</view>
<view class="table-td" style="width: 14%;">{{ item.goods_num }}</view>
<view class="table-td" style="width: 14%;">{{ item.after_store_stock }}</view>
<view class="table-td" style="width: 14%;">{{ item.goods_price }}</view>
<view class="table-td" style="width: 17%;">{{ $util.timeFormat(item.create_time) }}</view>
</view>
</scroll-view>
</view>
<!-- 分页 -->
<view class="pagination">
<uni-pagination @change="changePage" :pageSize="page_size" show-icon="true" :total="total" :current="page"/>
</view>
</view>
</template>
<script>
import {getStockGoodsRecords} from '@/api/stock.js'
export default {
data() {
return {
page: 1,
page_size: 8,
list: [],
total: 0
};
},
props: {
goodsId: {
type: Number,
default: () => {
return 0;
}
}
},
mounted() {
this.getData();
},
methods: {
// 分页发生变化
changePage(e) {
this.page = e.current;
this.getData();
},
// 获取数据
getData() {
getStockGoodsRecords({
page: this.page,
page_size: this.page_size,
goods_id: this.goodsId,
}).then(res => {
if (res.code >= 0) {
this.total = res.data.count;
this.list = res.data.list.map((item, index) => {
let unit = '';
if (item.type == 'input') {
unit = '+';
} else {
unit = '-';
}
item.goods_num = unit + item.goods_num;
return item;
});
}
})
},
// 弹窗关闭
close() {
this.$emit('close');
}
}
};
</script>
<style lang="scss" scoped>
.ns-record {
width: 100%;
height: 100%;
background: #ffffff;
border-radius: 0.04rem;
min-height: 2rem;
padding-bottom: 0.4rem;
.title {
width: 100%;
height: 0.5rem;
border-bottom: 0.01rem solid #e6e6e6;
font-size: 0.16rem;
line-height: 0.5rem;
text-align: center;
position: relative;
font-weight: bold;
.iconguanbi1 {
font-size: 0.2rem;
position: absolute;
top: 50%;
right: 0.15rem;
transform: translateY(-50%);
font-weight: 500;
}
}
.table {
width: 100%;
height: 4rem;
padding: 0 0.15rem;
box-sizing: border-box;
.table-th {
width: 100%;
height: 0.5rem;
display: flex;
align-items: center;
justify-content: space-between;
background: #f7f8fa;
padding: 0 0.15rem;
box-sizing: border-box;
}
.table-tb {
width: 100%;
height: calc(100% - 0.5rem);
.table-tr {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 0.5rem;
padding: 0 0.15rem;
box-sizing: border-box;
border-bottom: 0.01rem solid #e6e6e6;
}
}
}
}
/deep/ .uni-date-single {
height: 0.3rem;
}
.table-td {
height: 100%;
line-height: 0.5rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
font-size: 0.14rem;
}
.pagination {
width: 100%;
margin-top: 0.2rem;
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<view class="ns-record">
<view class="title">
价格库存
<text class="iconfont iconguanbi1" @click="close"></text>
</view>
<view class="table">
<view class="table-th">
<view class="table-td" style="min-width: 35%;max-width: 35%;">规格名称</view>
<view class="table-td">统一售价</view>
<view class="table-td" v-if="isUnifyPrice == 0">独立售价</view>
<view class="table-td">库存</view>
<view class="table-td" v-if="disabled">限制起送</view>
</view>
<scroll-view scroll-y="true" class="table-tb">
<view class="table-tr" v-for="(item, index) in skuList" :key="index">
<view class="table-td" style="min-width: 35%;max-width: 35%;">
{{ item.spec_name ? item.spec_name : item.goods_name }}</view>
<view class="table-td">{{ item.discount_price }}</view>
<view class="table-td input-wrap" v-if="isUnifyPrice == 0">
<input class="input" v-model="item.store_price" type="digit" />
<view class="unit"></view>
</view>
<view class="table-td input-wrap" v-if="!disabled">
<block v-if="globalStoreInfo.stock_type == 'store'">
<input class="input" v-model="item.stock" type="digit" :disabled="disabled" />
<view class="unit">{{ item.unit ? item.unit : '件' }}</view>
</block>
<view v-else>{{ item.stock }}</view>
</view>
<view class="table-td input-wrap" v-else>
<view>{{ item.stock }}</view>
</view>
<view class="table-td input-wrap" v-if="disabled">
<switch @change="changeswitch($event,index)" color="#FA6400" style="transform:scale(0.7)"
:checked="item.is_delivery_restrictions == 1" />
</view>
</view>
</scroll-view>
</view>
<view class="pop-bottom"><button class="primary-btn" @click="save()">确定</button></view>
</view>
</template>
<script>
import {
editGoods,
setGoodsLocalRestrictions
} from '@/api/goods.js';
export default {
data() {
return {
goodsSkuList: [],
flag: true
};
},
props: {
skuList: {
type: Array,
default: () => {
return [];
}
},
isUnifyPrice: {
type: Number,
default: () => {
return 0;
}
},
disabled: {
type: Boolean,
default: () => {
return false;
}
},
},
created() {},
methods: {
changeswitch(e, index) {
this.skuList[index].is_delivery_restrictions = e.detail.value ? 1 : 0
},
getGoodsSku() {
this.goodsSkuList = [];
Object.keys(this.skuList).forEach(key => {
let data = this.skuList[key];
let obj = {}
if (this.disabled) {
obj.sku_id = data.sku_id
obj.is_delivery_restrictions = data.is_delivery_restrictions
} else {
obj.sku_id = data.sku_id
obj.price = data.store_price ? data.store_price : data.discount_price
}
if (this.globalStoreInfo.stock_type == 'store') {
obj.stock = data.stock;
}
this.goodsSkuList.push(obj);
});
},
save() {
this.getGoodsSku();
if (!this.flag) return false;
this.flag = false;
uni.showLoading({
title: '请求处理中'
});
if (this.disabled) {
setGoodsLocalRestrictions({
goods_sku_list: JSON.stringify(this.goodsSkuList)
}).then(res => {
uni.hideLoading();
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.$root.$refs.goodsListTable.load();
this.close();
} else {
this.flag = true;
}
})
} else {
editGoods({
goods_sku_list: JSON.stringify(this.goodsSkuList)
}).then(res => {
uni.hideLoading();
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.$root.$refs.goodsListTable.load();
this.close();
} else {
this.flag = true;
}
})
}
},
// 弹窗关闭
close() {
this.$emit('close');
}
}
};
</script>
<style lang="scss" scoped>
.ns-record {
width: 100%;
height: 100%;
background: #ffffff;
border-radius: 0.04rem;
min-height: 2rem;
.title {
width: 100%;
height: 0.5rem;
border-bottom: 0.01rem solid #e6e6e6;
font-size: 0.16rem;
line-height: 0.5rem;
text-align: center;
position: relative;
font-weight: bold;
.iconguanbi1 {
font-size: 0.2rem;
position: absolute;
top: 50%;
right: 0.15rem;
transform: translateY(-50%);
font-weight: 500;
}
}
.table {
width: 100%;
height: 4rem;
padding: 0.15rem;
box-sizing: border-box;
.table-th {
width: 100%;
height: 0.5rem;
display: flex;
align-items: center;
justify-content: space-between;
background: #f7f8fa;
box-sizing: border-box;
}
.table-td {
display: flex;
flex: 1;
padding: 0 0.15rem;
}
.table-tb {
width: 100%;
height: calc(100% - 0.5rem);
.table-tr {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 0.5rem;
box-sizing: border-box;
border-bottom: 0.01rem solid #e6e6e6;
}
}
}
}
/deep/ .uni-date-single {
height: 0.3rem;
}
.input-wrap {
height: 100%;
line-height: 0.5rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
font-size: 0.14rem;
display: flex;
align-items: center;
.input {
width: 0.7rem;
height: 0.35rem;
border: 0.01rem solid #e6e6e6;
padding: 0 0.1rem;
border-top-left-radius: 0.03rem;
border-bottom-left-radius: 0.03rem;
}
.unit {
background-color: #eee;
height: 0.35rem;
width: 0.35rem;
text-align: center;
line-height: 0.35rem;
border: 0.01rem solid #e6e6e6;
border-left: 0;
border-top-right-radius: 0.03rem;
border-bottom-right-radius: 0.03rem;
}
}
.save {
height: 0.3rem;
line-height: 0.3rem;
}
.pop-bottom {
padding: 0.1rem 0.2rem;
border-top: 0.01rem solid #eee;
button {
width: 100%;
line-height: 0.35rem;
height: 0.35rem;
}
}
</style>

View File

@@ -0,0 +1,828 @@
var self;
import {
getGoodsList,
getGoodsCategory,
getServiceCategory,
getServiceList,
getGoodsSkuList,
getElectronicScaleInformation,
getGoodsInfoByCode
} from '@/api/goods'
import {mapGetters} from 'vuex';
export default {
name: 'nsGoods',
props: {
indexFocus: {
type: [String, Number],
default: ''
}
},
data() {
return {
type: 'goods',
serviceCategory: [],
serviceCategoryId: 'all',
serviceData: {
size: 12,
index: 1,
total: 0,
list: []
},
goodsCategoryId: 'all',
goodsCategory: [],
goodsData: {
size: 30,
index: 1,
total: 0,
list: []
},
skuInfo: null,
allSku: null,
searchText: '',
itemNum: 3,
mediaQueryOb: null,
paymentMoney: '',
cashierScale: null,
actionIndex: 0,
inputFocus: false,
goodsCategoryShow: false,
goodsCategoryIndex: 0,
serviceCategoryShow: false,
serviceCategoryIndex: 0,
scanCode: {
code: '',
lastTime: 0
},
isGoodsLoad: false,
goodsItems:{},//点击商品后如果是可转换库存商品存储商品数据
scanCodeSearch:false,
goodsNextPage: true,
};
},
watch: {
serviceCategoryId: function (nval) {
this.serviceData.page = 0;
this.serviceData.index = 1;
this.$refs.loading.show();
this.getService();
},
goodsCategoryId: function (nval) {
this.goodsData.page = 0;
this.goodsData.index = 1;
this.$refs.goodsLoading.show();
this.getGoods();
},
type: function () {
this.searchText = '';
}
},
computed: {
goodsSpec() {
if (this.allSku && this.skuInfo) {
let data = [];
if (this.skuInfo.goods_class != 6 || (this.skuInfo.goods_class == 6 && this.skuInfo.goods_spec_format)) {
data = this.allSku['sku_id_' + this.skuInfo.sku_id].goods_spec_format;
}
return data;
}
return [];
},
...mapGetters(['billingGoodsIds', 'billingGoodsData', 'billingActive', 'billingIsScanTrigger','billingIsShowCashBox'])
},
created() {
this.init();
this.addScanCodeEvent();
this.addKeyDownEvent();
},
mounted() {
this.mediaQueryOb = uni.createMediaQueryObserver(this);
this.mediaQueryOb.observe({maxWidth: 1500}, matches => {
if (matches) this.itemNum = 2;
});
this.mediaQueryOb.observe({minWidth: 1501, maxWidth: 1700}, matches => {
if (matches) this.itemNum = 3;
});
this.mediaQueryOb.observe({minWidth: 1701}, matches => {
if (matches) this.itemNum = 4;
});
document.addEventListener('click', this.isListShow)
},
beforeDestroy() {
document.removeEventListener("click", this.isListShow);
},
methods: {
nextGoodsLiist(){
if(!this.goodsNextPage) return;
this.goodsData.index ++;
this.getGoods();
},
init() {
self = this;
this.getServiceCategoryFn();
this.getGoodsCategoryFn();
this.getService();
this.getGoods();
},
getServiceCategoryFn() {
getServiceCategory().then(res => {
if (res.code == 0 && res.data) {
this.serviceCategory = res.data;
}
});
},
getGoodsCategoryFn() {
getGoodsCategory().then(res => {
if (res.code == 0 && res.data) {
this.goodsCategory = res.data;
}
});
},
getService() {
getServiceList({
page: this.serviceData.index,
page_size: this.serviceData.size,
category: this.serviceCategoryId,
search_text: this.searchText,
status: 1
}).then(res => {
if (this.$refs.loading) this.$refs.loading.hide();
if (res.code == 0) {
this.serviceData.total = res.data.count;
this.serviceData.list = res.data.list || [];
this.searchText = '';
}
});
},
getGoods() {
this.isGoodsLoad = false;
getGoodsList({
page: this.goodsData.index,
page_size: this.goodsData.size,
category: this.goodsCategoryId,
search_text: this.searchText,
goods_class: '1,6',
status: 1,
scene:'billing',
}).then((res) => {
if (this.$refs.goodsLoading) this.$refs.goodsLoading.hide();
if (res.code == 0) {
this.goodsData.total = res.data.count;
// this.goodsData.list = res.data.list || [];
res.data.list.forEach(item => {
item.adjust = {}; // 存储规格调价
});
if (this.goodsData.index == 1) this.goodsData.list = []; //如果是第一页需手动制空列表
this.goodsData.list.push(...res.data.list);
if(res.data.list.length) this.goodsNextPage = true;
else this.goodsNextPage = false;
if(this.scanCodeSearch) this.searchText = '';
this.scanCodeSearch = false;
}
this.isGoodsLoad = true;
})
},
goodsSelect(data, index) {
if (this.type == 'goods' && !data.stock) return;
if (index != undefined) this.actionIndex = index;
if (data.goods_class != 6 && data.adjust && data.adjust['sku_id_' + data.sku_id]) {
data.adjust_price = data.adjust['sku_id_' + data.sku_id].adjust_price;
data.is_adjust = data.adjust['sku_id_' + data.sku_id].is_adjust;
}
// 满足条件:多规格,称重商品计重模式,编辑数量
if (data.goods_spec_format || (data.goods_class == 6 && data.pricing_type == 'weight') || (data.status && data.status == 'edit')) {
this.setActive('SelectGoodsSku');
getGoodsSkuList(data.goods_id).then(res => {
if (res.code == 0) {
let obj = {};
res.data.forEach(item => {
try{
item.goods_spec_format = JSON.parse(item.goods_spec_format);
}catch(e){
item.goods_spec_format = [];
}
obj['sku_id_' + item.sku_id] = item;
});
this.allSku = obj;
this.skuInfo = obj['sku_id_' + data.sku_id];
// 调整价格,称重商品不参与永久调价,每次都是初始原价
if (data.goods_class != 6 && data.adjust_price) {
this.skuInfo.adjust_price = data.adjust_price; // 使用调价
} else {
this.skuInfo.adjust_price = data.price; // 未调价,使用原价
}
this.skuInfo.adjust_price = parseFloat(this.skuInfo.adjust_price).toFixed(2);
this.$set(this.skuInfo, 'status', data.status || '');
this.$set(this.skuInfo, 'editKey', data.editKey || '');
if (data.goods_class != 6) {
this.skuInfo.num = data.num || 1; // 默认购买数量为1
}
// 称重商品,计重模式
if (data.goods_class == 6 && data.pricing_type == 'weight') {
let num = data.num || '';
if (num) {
this.$set(this.skuInfo, 'weigh', num);
Object.values(this.allSku).forEach((item, index) => {
this.$set(item, 'weigh', num);
});
}
// 打开收银秤
this.openCashierScale()
}
this.$refs.skuPopup.open();
setTimeout(() => {
this.inputFocus = true;
}, 200);
}
});
} else {
this.handleSelectGoods(data);
this.scanCode = {
lastTime: 0,
code: ''
};
this.$store.commit('billing/setActive', 'SelectGoodsAfter'); // 记录页面当前活跃值:选择完商品
}
},
skuSelect(sku_id) {
if (!this.skuInfo.status) {
this.skuInfo = this.allSku['sku_id_' + sku_id];
this.goodsItems.sku_id = sku_id
let skuData = this.goodsData.list[this.actionIndex];
// 调整价格
if (skuData.adjust && skuData.adjust['sku_id_' + sku_id]) {
this.skuInfo.adjust_price = skuData.adjust['sku_id_' + sku_id].adjust_price;
this.skuInfo.is_adjust = skuData.adjust['sku_id_' + sku_id].is_adjust;
} else {
// 使用原价
this.skuInfo.adjust_price = this.skuInfo.price;
}
this.skuInfo.adjust_price = parseFloat(this.skuInfo.adjust_price).toFixed(2);
if (this.skuInfo.goods_class != 6) {
this.skuInfo.num = this.skuInfo.num || 1; // 默认购买数量为1
}
}
},
skuConfirm() {
if (!this.skuInfo) return;
if (this.skuInfo.stock <= 0) {
this.$util.showToast({
title: '商品库存不足'
});
return;
}
if (this.skuInfo.price.length == 0 || this.skuInfo.adjust_price.length == 0) {
this.$util.showToast({
title: '请输入单价'
});
return;
}
if (this.skuInfo.price < 0 || this.skuInfo.adjust_price < 0) {
this.$util.showToast({
title: '单价不能小于0'
});
return;
}
if (this.skuInfo.goods_class != 6) {
if (Number.parseInt(this.skuInfo.num) <= 0 || !/^\d{0,10}$/.test(Number.parseInt(this.skuInfo.num))) {
this.$util.showToast({
title: '请输入合法的数值,数值要大于零'
});
return;
}
if (this.skuInfo.stock < this.skuInfo.num) {
this.$util.showToast({
title: '商品库存不足'
});
return;
}
}
if (this.skuInfo.goods_class == 6 && this.skuInfo.pricing_type == 'weight') {
if (Number.parseFloat(this.skuInfo.weigh) <= 0 || !/^\d{0,10}(.?\d{0,3})$/.test(Number.parseFloat(this.skuInfo.weigh))) {
this.$util.showToast({
title: '请输入合法的数值,数值要大于零且小数位不能超过三位'
});
return;
}
if (this.skuInfo.stock < this.skuInfo.weigh) {
this.$util.showToast({
title: '商品库存不足'
});
return;
}
try {
this.$pos.send('CloseWeigher');
} catch (e) {
}
}
this.skuInfo.is_adjust = this.skuInfo.adjust_price != this.skuInfo.price;
// 设置右侧商品的调价
if (!this.goodsData.list[this.actionIndex].adjust['sku_id_' + this.skuInfo.sku_id]) {
this.goodsData.list[this.actionIndex].adjust['sku_id_' + this.skuInfo.sku_id] = {};
}
this.goodsData.list[this.actionIndex].adjust['sku_id_' + this.skuInfo.sku_id].adjust_price = this.skuInfo.adjust_price;
this.goodsData.list[this.actionIndex].adjust['sku_id_' + this.skuInfo.sku_id].is_adjust = this.skuInfo.is_adjust;
this.handleSelectGoods(this.skuInfo);
this.scanCode = {
lastTime: 0,
code: ''
};
this.$store.commit('billing/setActive', 'SelectGoodsAfter'); // 记录页面当前活跃值:选择完商品
this.$refs.skuPopup.close();
},
search() {
switch (this.type) {
case 'service':
this.serviceData.page = 0;
this.serviceData.index = 1;
this.$refs.loading.show();
this.getService();
break;
case 'goods':
this.goodsData.page = 0;
this.goodsData.index = 1;
this.$refs.goodsLoading.show();
this.getGoods();
break;
}
},
destroyed() {
this.mediaQueryOb.disconnect();
},
switchStoreAfter() {
this.serviceCategory = [];
this.serviceCategoryId = 'all';
this.serviceData = {
size: 12,
index: 1,
page: 0,
total: 1,
list: []
};
this.goodsCategoryId = 'all';
this.goodsCategory = [];
this.goodsData = {
size: 30,
index: 1,
page: 0,
total: 1,
list: []
};
this.getServiceCategoryFn();
this.getGoodsCategoryFn();
this.getService();
this.getGoods();
},
pageChange(e) {
if (this.type == 'goods') {
this.goodsData.index = e.current;
this.getGoods();
} else if (this.type == 'service') {
this.serviceData.index = e.current;
this.getService();
}
},
switchItem(type) {
this.type = type;
if (this.type == 'goods') {
this.goodsData.index = 1;
this.getGoods();
} else if (this.type == 'service') {
this.serviceData.index = 1;
this.getService();
} else if (this.type == 'money') {
// 无码商品
this.setActive('UnnumberedGoods');
}
},
keydown(value) {
let arr = this.paymentMoney.split('.');
if (arr[1]) {
if (value == '.' || arr[1].length == 2) return;
if (value == '00' && arr[1].length == 1) value = '0';
}
if (parseFloat(this.paymentMoney + value) > 1000000) {
this.$util.showToast({
title: '最大不能超过1000000'
});
return;
}
this.paymentMoney += value;
},
deleteCode() {
this.paymentMoney = this.paymentMoney.substr(0, this.paymentMoney.length - 1);
},
paymentMoneyConfirm() {
if (!this.paymentMoney.length) {
this.$util.showToast({
title: '请输入收款金额'
});
return;
}
if (isNaN(parseFloat(this.paymentMoney)) || !/^(([0-9][0-9]*)|(([0]\.\d{1,2}|[1-9][0-9]*\.\d{1,2})))$/.test(parseFloat(this.paymentMoney))) {
this.$util.showToast({
title: '收款金额格式错误'
});
return;
}
if (this.paymentMoney <= 0) {
this.$util.showToast({
title: '收款金额不能小于等于0'
});
return;
}
if (parseFloat(this.paymentMoney) > 1000000) {
this.$util.showToast({
title: '最大不能超过1000000'
});
return;
}
this.handleSelectGoods({
goods_id: parseInt(new Date().getTime() / 1000),
sku_id: parseInt(new Date().getTime() / 1000),
num: 1,
money: parseFloat(this.paymentMoney)
});
this.scanCode = {
lastTime: 0,
code: ''
};
this.$store.commit('billing/setActive', 'SelectGoodsAfter'); // 记录页面当前活跃值:选择完商品
this.paymentMoney = '';
},
/**
* 打开收银秤
*/
openCashierScale() {
if (this.addon.includes('scale')) {
if (!this.cashierScale) {
getElectronicScaleInformation().then(res => {
if (res.code == 0 && res.data) {
this.cashierScale = res.data.config;
try {
this.$pos.send('OpenWeigher', `OS2X:${this.cashierScale.serialport}:${this.cashierScale.baudrate}`);
} catch (e) {
this.cashierScale = null
}
}
})
} else {
try {
this.$pos.send('OpenWeigher', `OS2X:${this.cashierScale.serialport}:${this.cashierScale.baudrate}`);
} catch (e) {
this.cashierScale = null
}
}
}
},
/**
* 去皮
*/
tare() {
this.$pos.send('Tare')
},
/**
* 清零
*/
zero() {
this.$pos.send('Zero');
},
setActive(key) {
this.$store.commit('billing/setActive', key);
},
paymentMoneyChange(event) {
if (this.paymentMoney.length == 0) {
// 如果没有输入则结账
this.setActive('SelectGoodsAfter')
} else {
this.setActive('UnnumberedGoods')
}
},
// 商品数量减少
dec(data) {
if (this.skuInfo) {
if (this.skuInfo.num <= 1 && this.skuInfo.stock > 0) {
this.skuInfo.num = 1;
} else if(this.skuInfo.stock > 0) {
this.skuInfo.num--;
}
this.$forceUpdate();
}
},
// 商品数量增加
inc(data) {
if (this.skuInfo) {
if (this.skuInfo.num >= this.skuInfo.stock && this.skuInfo.stock > 0) {
this.skuInfo.num = this.skuInfo.stock;
} else if(this.skuInfo.stock > 0){
this.skuInfo.num++;
}
this.$forceUpdate();
}
},
// 打开钱箱
openCashBox() {
this.$emit('openCashBox')
},
setGoodsCategoryShow(id, index) {
if (!this.goodsCategoryShow) {
if (id === 'all') {
if (this.goodsCategory.length > 13) {
this.goodsCategoryShow = !this.goodsCategoryShow
} else {
this.goodsCategoryId = id
}
} else {
this.goodsCategoryId = id
}
} else {
this.goodsCategoryId = id;
this.goodsCategoryIndex = index;
this.goodsCategoryShow = false
}
},
setServiceCategoryShow(id, index) {
if (!this.serviceCategoryShow) {
if (id === 'all') {
if (this.serviceCategory.length > 13) {
this.serviceCategoryShow = !this.serviceCategoryShow
} else {
this.serviceCategoryId = id
}
} else {
this.serviceCategoryId = id
}
} else {
this.serviceCategoryId = id;
this.serviceCategoryIndex = index;
this.serviceCategoryShow = false
}
},
isListShow() {
this.goodsCategoryShow = false;
this.serviceCategoryShow = false;
},
// 获取商品信息通过条形码
getSkuByCode(code) {
if (this.type != 'goods' || this.billingActive == 'ShowMember' || this.billingActive == 'OrderCreate') return;
code = code.toString().trim();
getGoodsInfoByCode(code).then(res => {
if (res.code == 0) {
if (res.data) {
if (res.data.goods_state == 0) {
this.$util.showToast({
title: '该商品已下架'
});
return;
}
if (res.data.stock == 0) {
this.$util.showToast({
title: '该商品库存不足!'
});
return;
}
this.handleSelectGoods(res.data);
this.scanCode = {
lastTime: 0,
code: ''
};
this.$store.commit('billing/setActive', 'SelectGoodsAfter'); // 记录页面当前活跃值:选择完商品
} else {
this.$util.showToast({
title: '未找到该商品!'
})
}
} else {
this.$util.showToast({
title: res.message,
})
}
})
},
// 处理选中商品数据
handleSelectGoods(data) {
let key = 'sku_' + data.sku_id;
let num = data.num || 1;
num = parseInt(num);
// 项目服务和称重商品每次都是新增重新定义key
if (data.goods_class == 4 || data.goods_class == 6) {
if (data.status == 'edit') {
// 编辑
key = data.editKey;
} else {
//新增
var index = 0;
Object.keys(this.billingGoodsData).forEach(k => {
if (k.indexOf(key) != -1) {
index++;
}
});
key += '_' + index;
}
}
// 称重商品,计价方式:计重
if (data.goods_class == 6 && data.pricing_type == 'weight') num = Number.parseFloat(data.weigh);
let _billingGoodsData = this.$util.deepClone(this.billingGoodsData);
// 已加入清单,追加数量
if (_billingGoodsData[key]) {
_billingGoodsData[key].num += num;
} else {
// 第一次加入清单,设置数据
_billingGoodsData[key] = this.$util.deepClone(data);
_billingGoodsData[key].num = num;
}
// 编辑商品数量
if (data.status && data.status == 'edit') {
_billingGoodsData[key].num = num;
}
// 称重商品每次覆盖数量
if (data.goods_class == 6 && data.pricing_type == 'weight') {
_billingGoodsData[key].num = parseFloat(_billingGoodsData[key].num.toFixed(3))
}
// 调整单价
if (data.adjust_price) {
_billingGoodsData[key].price = parseFloat(data.adjust_price);
_billingGoodsData[key].adjust_price = parseFloat(data.adjust_price);
}
_billingGoodsData[key].is_adjust = data.is_adjust || false;
this.$store.commit('billing/setGoodsData', _billingGoodsData);
},
/**
* 添加扫码监听事件
*/
addScanCodeEvent() {
// #ifdef APP-PLUS
plus.key.addEventListener('keyup', this.listenerScanCode, true);
// #endif
// #ifdef H5
window.addEventListener('keypress', this.listenerScanCode, true);
// #endif
},
/**
* 移除扫码监听事件
*/
removeScanCodeEvent() {
// #ifdef APP-PLUS
plus.key.removeEventListener('keyup', this.listenerScanCode, true);
// #endif
// #ifdef H5
window.removeEventListener('keypress', this.listenerScanCode, true);
// #endif
},
/**
* 监听扫码事件
* @param {Object} e
*/
listenerScanCode(e) {
const clearBarCode = () => {
this.scanCode = {
lastTime: 0,
code: ''
};
this.$store.commit('billing/setIsScanTrigger', false);
};
// #ifdef H5
var currCode = e.keyCode || e.which || e.charCode;
// #endif
// #ifdef APP-PLUS
const keyArr = {
'keycode_7': 0,
'keycode_8': 1,
'keycode_9': 2,
'keycode_10': 3,
'keycode_11': 4,
'keycode_12': 5,
'keycode_13': 6,
'keycode_14': 7,
'keycode_15': 8,
'keycode_16': 9
};
var currCode = keyArr['keycode_' + e.keyCode] || '';
// #endif
var currTime = new Date().getTime();
if (this.scanCode.lastTime > 0) {
if (currTime - this.scanCode.lastTime <= 100) {
this.scanCode.code += String.fromCharCode(currCode);
this.$store.commit('billing/setIsScanTrigger', true);
} else if (currTime - this.scanCode.lastTime > 500) {
// 输入间隔500毫秒清空
clearBarCode();
}
} else {
this.scanCode.code = String.fromCharCode(currCode);
}
this.scanCode.lastTime = currTime;
// #ifdef H5
var code = 13;
// #endif
// #ifdef APP-PLUS
var code = 66;
// #endif
if (currCode == code) {
// 扫码枪
if (this.scanCode.code && this.scanCode.code.length >= 8) {
this.scanCodeSearch = true;
this.getSkuByCode(this.scanCode.code);
this.$store.commit('billing/setIsScanTrigger', true);
}
// 回车输入后清空
clearBarCode();
}
},
/**
* 添加键盘监听事件
*/
addKeyDownEvent() {
// #ifdef H5
window.addEventListener("keydown", this.listenerKeyDown, true);
// #endif
},
/**
* 移除键盘监听事件
*/
removeKeyDownEvent() {
// #ifdef H5
window.removeEventListener("keydown", this.listenerKeyDown, true);
// #endif
},
// 监听键盘按下事件
listenerKeyDown(e) {
var code = e.code;
if (this.billingActive == 'SelectGoodsSku') {
// 打开商品规格项弹出框
if (code == 'Enter' || code == 'NumpadEnter') {
this.skuConfirm();
}
} else if (this.billingActive == 'UnnumberedGoods') {
// 无码商品
if (code == 'Enter' || code == 'NumpadEnter') {
if (!this.billingIsScanTrigger) this.paymentMoneyConfirm();
}
}
},
},
};
/**
* 监听重量变化
* @param {Object} text
*/
window.WEIGHER_DATA_CALLBACK = function (text) {
let data = text.split(':');
self.$set(self.skuInfo, 'weigh', data[0] != '-' ? data[0] : '')
};

View File

@@ -0,0 +1,628 @@
.container {
height: 100%;
}
.header-action {
padding: 0.22rem 0.24rem;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 0.04rem;
margin-bottom: 0.2rem;
height: 0.88rem;
.header-action-left {
display: flex;
align-items: center;
height: 0.44rem;
background-color: var(--primary-color-light-9);
border-radius: 0.22rem;
view {
min-width: 1.02rem;
height: 0.44rem;
line-height: 0.44rem;
text-align: center;
font-size: 0.14rem;
border-left-width: 0;
transition: all 0.3s;
cursor: pointer;
border-radius: 0.22rem;
color: $primary-color;
&.active {
color: #fff;
background-color: $primary-color;
}
}
}
.header-action-center {
margin-left: 0.29rem;
.left {
margin-right: 0.24rem;
}
view {
min-width: 1.02rem;
height: 0.44rem;
line-height: 0.44rem;
text-align: center;
font-size: 0.14rem;
border-left-width: 0;
transition: all 0.3s;
cursor: pointer;
border-radius: 0.22rem;
color: #fff;
background-color: $primary-color;
}
}
.header-action-right {
display: flex;
align-items: center;
height: 0.4rem;
border-radius: 0.2rem;
border: 0.01rem solid #CCCCCC;
overflow: hidden;
box-sizing: border-box;
input {
background: #ffffff;
border-radius: 0.02rem;
height: 0.36rem;
padding-left: 0.24rem;
font-size: 0.14rem;
width: 2.80rem;
box-sizing: border-box;
}
.search-btn {
width: 0.36rem;
height: 0.36rem;
background: #ffffff;
border-radius: 0.02rem;
border-left: 0;
text-align: center;
line-height: 0.36rem;
cursor: pointer;
}
}
}
.content {
position: relative;
box-sizing: border-box;
height: calc(100% - 1.08rem);
transform: rotate(0);
display: flex;
.table-pages {
display: flex;
justify-content: flex-end;
position: absolute;
bottom: 0;
left: 1.72rem;
right: 0;
background-color: #fff;
padding: 0.24rem 0.20rem;
z-index: 1;
}
}
.type-switch {
width: 1.52rem;
margin-right: 0.2rem;
padding: 0.24rem 0;
font-size: 0.14rem;
white-space: nowrap;
box-sizing: border-box;
border-radius: 0.04rem;
position: relative;
.list {
height: calc(100% - 0.6rem) !important;
}
.list-all {
position: absolute;
width: 0;
height: 100%;
left: 0;
top: 0;
z-index: 99;
box-shadow: 0.04rem 0 0.12rem 0 rgba(0, 0, 0, 0.04);
transition: width 0.3s;
box-sizing: border-box;
overflow: hidden;
border-radius: 0.04rem;
&.show {
width: 4.08rem;
height: 100%;
}
.center {
width: 4.08rem;
height: 100%;
padding: 0.24rem;
overflow-y: auto;
flex-wrap: wrap;
.switch-item {
margin-right: 0.24rem;
width: 1.04rem;
&:nth-child(2n+2){
margin-right: 0;
}
}
}
}
/deep/ .uni-scroll-view-content {
display: flex;
flex-direction: column;
// align-items: center;
}
// /deep/ .uni-scroll-view {
// width: 1.1rem !important;
// }
.switch-item {
width: 1.04rem;
border-radius: 0.04rem;
padding: 0.1rem 0.1rem;
font-size: 0.14rem;
text-align: center;
margin-left: 0.24rem;
cursor: pointer;
box-sizing: border-box;
border: 0.01rem solid #DCDEE2;
.item-title{
white-space: normal;
max-height: 0.4rem;
overflow: hidden;
word-break: break-all;
}
}
.all-category{
.switch-item {
height: 0.4rem;
width: 1.04rem;
border-radius: 0.04rem;
padding: 0 0.1rem;
font-size: 0.14rem;
text-align: center;
line-height: 0.4rem;
margin-left: 0.24rem;
cursor: pointer;
overflow: hidden;
box-sizing: border-box;
border: 0.01rem solid #DCDEE2;
}
}
.switch-item.active {
background-color: $primary-color;
border-color: $primary-color;
color: #fff;
}
.switch-item:not(:last-child) {
margin-bottom: 0.20rem;
}
}
.list-wrap {
position: relative;
flex: 1;
padding-bottom: 0.78rem;
}
.money-pages {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.money-wrap {
background: #fff;
border-radius: 0.05rem;
padding-top: 0.3rem;
.head {
height: 0.6rem;
line-height: 0.6rem;
text-align: center;
font-weight: bold;
position: relative;
text {
font-size: 0.16rem;
}
.iconguanbi1 {
position: absolute;
right: 0.15rem;
font-size: 0.22rem;
cursor: pointer;
}
}
.content-wrap {
display: flex;
border: 0.01rem solid #e6e6e6;
height: 0.6rem;
align-items: center;
margin: 0 0.2rem;
padding: 0 0.15rem;
.unit {
font-size: 0.25rem;
}
.money {
margin-left: 0.05rem;
font-size: 0.25rem;
}
}
.keyboard-wrap {
width: 4rem;
padding: 0 0.2rem 0.3rem 0.2rem;
margin-top: 0.1rem;
}
}
}
.table-list {
display: flex;
align-items: center;
flex-wrap: wrap;
width: calc(100% + 0.12rem);
.table-item {
border: 0.01rem solid #fff;
box-sizing: border-box;
padding: 0.1rem 0.18rem 0.1rem 0.1rem;
background-color: #fff;
margin-bottom: 0.12rem;
margin-right: 0.12rem;
cursor: pointer;
transition: border-color, background-color 0.3s;
position: relative;
border-radius: 0.04rem;
&.item-mum-2{
width: calc((100% - 0.25rem) / 2);
}
&.item-mum-3{
width: calc((100% - 0.37rem) / 3);
}
&.item-mum-4{
width: calc((100% - 0.49rem) / 4);
}
&:focus{
outline: none;
}
.item-other {
display: flex;
flex-wrap: wrap;
margin-left: 0.1rem;
}
.item-img {
width: 0.9rem;
height: 0.9rem;
display: flex;
align-items: center;
overflow: hidden;
-ms-flex-negative: 0;
-webkit-flex-shrink: 0;
flex-shrink: 0;
image {
width: 100%;
border-radius: 0.03rem;
}
}
.item-name {
height: 0.4rem;
line-height: 0.2rem;
max-width: 1.58rem;
margin-bottom: 0.05rem;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-info {
cursor: pointer;
flex: 1;
display: flex;
.item-time {
font-size: 0.12rem;
color: #909399;
}
.item-money {
font-size: 0.14rem;
color: $primary-color;
height: 0.19rem;
line-height: 0.19rem;
}
.item-stock {
height: 0.17rem;
font-size: 0.12rem;
color: #808695;
line-height: 0.17rem;
}
}
.no-stock {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
cursor: not-allowed;
image {
height: 60%;
}
}
}
.table-item.yes-stock {
&:hover {
background-color: var(--primary-color-light-9);
border-color: $primary-color;
}
&.focus,
&:focus {
background-color: var(--primary-color-light-9);
border-color: $primary-color;
outline: none;
}
&.active {
border-color: $primary-color;
background-color: $primary-color;
color: #fff;
.item-time,
.item-money,
.item-stock {
color: #fff;
}
}
}
}
.sku-wrap {
display: flex;
flex-direction: column;
width: 7rem;
height: 4rem;
background-color: #fff;
border-radius: 0.04rem;
box-shadow: 0 0.01rem 0.12rem 0 rgba(0, 0, 0, 0.1);
.header,
.footer {
height: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 0.01rem solid #e6e6e6;
position: relative;
.title {
font-size: 0.16rem;
font-weight: bold;
}
.iconfont {
line-height: 1;
position: absolute;
right: 0.15rem;
font-size: 0.2rem;
cursor: pointer;
}
}
.body {
flex: 1;
padding: 0.15rem;
scroll-view {
height: 2.55rem;
}
}
.spec {
line-height: 0.35rem;
font-weight: bold;
margin-top: 0.1rem;
}
.spec-value {
.value-item {
display: inline-block;
height: 0.3rem;
line-height: 0.3rem;
background-color: #fff;
color: #333;
border: 0.01rem solid #e6e6e6;
margin: 0.05rem 0.1rem 0 0;
padding: 0 0.1rem;
cursor: pointer;
transition: all 0.3s;
border-radius: 0.04rem;
&:hover,
&.active {
background: var(--primary-color-light-9);
border-color: $primary-color;
color: $primary-color;
}
&.disabled {
color: #999;
cursor: not-allowed;
background: #f7f7f7;
border-color: #f7f7f7;
}
}
}
.spec-value-form {
display: flex;
align-items: center;
.spec-value-input {
margin-right: 0.05rem;
border-width: 0.01rem;
border-style: solid;
background-color: #fff;
color: rgba(0, 0, 0, 0.85);
padding-left: 0.1rem;
height: 0.38rem;
line-height: 0.38rem;
font-size: 0.14rem;
border-color: #e6e6e6;
border-radius: 0.02rem;
width: 2rem;
}
.num-dec {
width: 0.25rem;
height: 0.25rem;
background: #e6e6e6;
border: 0.01rem solid #e6e6e6;
border-radius: 30%;
text-align: center;
line-height: 0.23rem;
font-size: 0.25rem;
margin-right: 0.1rem;
cursor: pointer;
transition: 0.3s;
}
.num-inc {
width: 0.25rem;
height: 0.25rem;
background: $primary-color;
border: 0.01rem solid #e6e6e6;
border-radius: 30%;
text-align: center;
line-height: 0.23rem;
font-size: 0.25rem;
margin-left: 0.1rem;
cursor: pointer;
transition: 0.3s;
color: #fff;
}
}
.scale-action {
button {
width: .8rem;
margin: .15rem .15rem 0 0;
}
}
.goods-info {
display: flex;
.image {
width: 0.7rem;
height: 0.7rem;
margin-right: 0.1rem;
flex-shrink: 0;
overflow: hidden;
image {
width: 100%;
}
}
.info {
display: flex;
flex-direction: column;
justify-content: space-between;
.price {
line-height: 1;
margin-top: 0.05rem;
}
}
}
.footer {
border: 0;
height: 0.6rem;
button {
width: 95%;
}
}
}
.empty {
text-align: center;
padding-top: 1.2rem;
image {
width: 2rem;
}
.tips {
color: #999;
margin-top: 0.15rem;
}
}
@media screen and (min-width: 450px) and (max-width: 950px) {
.type-switch .list-all.show {
width: 2.78rem;
.center{
width: 2.78rem;
.switch-item:nth-child(2n+2) {
margin-right: 0;
}
.switch-item:nth-child(3n+3) {
margin-right: 0.24rem;
}
}
}
}
@media screen and (min-width: 1201px) and (max-width: 1900px) {
.type-switch .list-all.show {
width: 6.6rem;
.center{
width: 6.6rem;
.switch-item:nth-child(5n+5) {
margin-right: 0;
}
.switch-item:nth-child(3n+3) {
margin-right: 0.24rem;
}
}
}
}

View File

@@ -0,0 +1,239 @@
<template>
<view class="container goods-container">
<view class="header-action common-wrap">
<view class="flex items-center">
<view class="header-action-left">
<view class="flex-1" :class="{ active: type == 'goods' }" @click="switchItem('goods')">商品</view>
<view class="flex-1" :class="{ active: type == 'service' }" @click="switchItem('service')">项目</view>
<view class="flex-1" :class="{ active: type == 'money' }" @click="switchItem('money')">无码商品</view>
</view>
<view class="header-action-center flex" v-if="billingIsShowCashBox">
<view class="flex-1" @click="openCashBox()">打开钱箱</view>
</view>
</view>
<view class="header-action-right">
<input placeholder="请输入商品/项目名称/编码" placeholder-style="font-size:0.14rem;color:#ACACAC;" v-model="searchText" @focus="setActive('inputSearchText')" @blur="setActive('')" @confirm="search" />
<view class="search-btn" @click="search">
<text class="iconfont icon31sousuo"></text>
</view>
</view>
</view>
<view class="content">
<view class="type-switch common-wrap flex-shrink-0 flex uni-column" v-if="serviceCategory.length && type == 'service'">
<view class="switch-item flex-shrink-0" :class="{ active: serviceCategoryId == 'all' }" @click.stop="setServiceCategoryShow('all',0)">所有分类</view>
<scroll-view scroll-y="true" :show-scrollbar="false" :scroll-into-view="'serviceCategory-' + serviceCategoryIndex" class="list flex-shrink-0 common-scrollbar">
<view :id="'serviceCategory-' + index" class="switch-item flex-shrink-0" :class="{ active: serviceCategoryId == item.category_id }" v-for="(item, index) in serviceCategory" :key="index" @click.stop="setServiceCategoryShow(item.category_id,index)">
<view class="item-title">{{ item.category_name }}</view>
</view>
</scroll-view>
<view v-if="serviceCategory.length > 13" class="list-all common-wrap" :class="{ 'show': serviceCategoryShow }" @click.stop="() => { return false }">
<view class="all-category center flex content-start">
<view class="switch-item flex-shrink-0" :class="{ active: serviceCategoryId == 'all' }" @click="setServiceCategoryShow('all', 0)">所有分类</view>
<view class="switch-item flex-shrink-0" :class="{ active: serviceCategoryId == item.category_id }" v-for="(item, index) in serviceCategory" :key="index" @click="setServiceCategoryShow(item.category_id, index)">
<view class="item-title">{{ item.category_name }}</view>
</view>
</view>
</view>
</view>
<view class="type-switch common-wrap flex-shrink-0 flex uni-column" v-if="goodsCategory.length && type == 'goods'">
<view class="switch-item flex-shrink-0" :class="{ active: goodsCategoryId == 'all' }" @click.stop.prevent="setGoodsCategoryShow('all',0)">所有分类</view>
<scroll-view scroll-y="true" :show-scrollbar="false" :scroll-into-view="'goodsCategory-' + goodsCategoryIndex" class="list flex-shrink-0 common-scrollbar" v-show="goodsCategory.length && type == 'goods'">
<view :id="'goodsCategory-' + index" class="switch-item flex-shrink-0" :class="{ active: goodsCategoryId == item.category_id }" v-for="(item, index) in goodsCategory" :key="index" @click.stop="setGoodsCategoryShow(item.category_id,index)">
<view class="item-title">{{ item.category_name }}</view>
</view>
</scroll-view>
<view v-if="goodsCategory.length > 13" class="list-all common-wrap" :class="{ 'show': goodsCategoryShow }" @click.stop="() => { return false }">
<view class="all-category center flex content-start">
<view class="switch-item flex-shrink-0" :class="{ active: goodsCategoryId == 'all' }" @click="setGoodsCategoryShow('all')">所有分类</view>
<view class="switch-item flex-shrink-0" :class="{ active: goodsCategoryId == item.category_id }" v-for="(item, index) in goodsCategory" :key="index" @click="setGoodsCategoryShow(item.category_id, index)">
<view class="item-title">{{ item.category_name }}</view>
</view>
</view>
</view>
</view>
<scroll-view scroll-y="true" class="list-wrap goods" v-show="type == 'goods'" @scrolltolower="nextGoodsLiist">
<view class="table-list" v-show="goodsData.list.length" data-focus="true">
<view class="table-item goods-select-focus goods-focus"
:class="{ 'yes-stock': item.stock>0, 'item-mum-2': itemNum == 2, 'item-mum-3': itemNum == 3, 'item-mum-4': itemNum == 4, active: billingGoodsIds.indexOf(item.goods_id) != -1, focus: indexFocus == index }"
v-for="(item, index) in goodsData.list" :key="index" @click="goodsSelect(item, index)" tabindex="0" :data-tab-index="index">
<view class="item-info">
<view class="item-img">
<image v-if="item.goods_image == '@/static/goods/goods.png'" src="@/static/goods/goods.png" mode="widthFix"/>
<image v-else :src="$util.img(item.goods_image.split(',')[0], { size: 'small' })" @error="item.goods_image = '@/static/goods/goods.png'" mode="widthFix"/>
</view>
<view class="item-other flex-1">
<view class="item-name multi-hidden">{{ item.goods_name }}</view>
<view class="w-full flex justify-between items-center self-end">
<view class="item-money">{{ item.price | moneyFormat }}</view>
<view class="item-stock">库存{{ item.stock }}</view>
</view>
</view>
</view>
<view class="no-stock" v-if="item.stock <= 0">
<image src="@/static/stock/stock_empty.png" mode="heightFix"/>
</view>
</view>
</view>
<view class="empty" v-show="isGoodsLoad && !goodsData.list.length">
<image src="@/static/goods/goods_empty.png" mode="widthFix"/>
<view class="tips">暂无商品</view>
</view>
<ns-loading :layer-background="{ background: 'rgba(255,255,255,.6)' }" :default-show="false" ref="goodsLoading"></ns-loading>
</scroll-view>
<scroll-view scroll-y="true" class="list-wrap service" v-show="type == 'service'">
<view class="table-list" v-show="serviceData.list.length">
<view class="table-item goods-select-focus service-focus"
:class="{ 'yes-stock': item.stock > 0, 'item-mum-2': itemNum == 2, 'item-mum-3': itemNum == 3, 'item-mum-4': itemNum == 4, active: billingGoodsIds.indexOf(item.goods_id) != -1, focus: indexFocus == index }"
v-for="(item, index) in serviceData.list" :key="index" @click="goodsSelect(item, index)" tabindex="0" :data-tab-index="index">
<view class="item-info">
<view class="item-img">
<image v-if="item.goods_image == '@/static/goods/goods.png'" src="@/static/goods/goods.png" mode="widthFix"/>
<image v-else :src="$util.img(item.goods_image.split(',')[0], { size: 'small' })" @error="item.goods_image = '@/static/goods/goods.png'" mode="widthFix"/>
</view>
<view class="item-other">
<view class="item-name multi-hidden">{{ item.goods_name }}</view>
<view class="w-full flex justify-between items-center self-end">
<view class="item-money">{{ item.price | moneyFormat }}</view>
<view class="item-time" v-if="item.service_length">时长{{ item.service_length }}分钟</view>
</view>
</view>
</view>
<view class="no-stock" v-if="item.stock <= 0">
<image src="@/static/stock/stock_empty.png" mode="heightFix"/>
</view>
</view>
</view>
<view class="empty" v-show="!serviceData.list.length">
<image src="@/static/goods/goods_empty.png" mode="widthFix"/>
<view class="tips">暂无商品</view>
</view>
<ns-loading :layer-background="{ background: 'rgba(255,255,255,.6)' }" :default-show="false" ref="loading"></ns-loading>
</scroll-view>
<view class="table-pages" v-show="type == 'service' && serviceData.list.length">
<uni-pagination :total="serviceData.total" :showIcon="true" @change="pageChange" :pageSize="serviceData.size" :value="serviceData.index" />
</view>
<!-- <view class="table-pages" v-show="type == 'goods' && goodsData.list.length">
<uni-pagination :total="goodsData.total" :showIcon="true" @change="pageChange" :pageSize="goodsData.size" :value="goodsData.index" />
</view> -->
<view class="money-pages" v-show="type == 'money'">
<view class="money-wrap">
<view class="content-wrap">
<view class="unit"></view>
<input type="text" class="money" v-model="paymentMoney" @input="paymentMoneyChange" />
</view>
<view class="keyboard-wrap">
<view class="num-wrap">
<view class="key-item" @click="keydown('1')">1</view>
<view class="key-item" @click="keydown('2')">2</view>
<view class="key-item" @click="keydown('3')">3</view>
<view class="key-item" @click="keydown('4')">4</view>
<view class="key-item" @click="keydown('5')">5</view>
<view class="key-item" @click="keydown('6')">6</view>
<view class="key-item" @click="keydown('7')">7</view>
<view class="key-item" @click="keydown('8')">8</view>
<view class="key-item" @click="keydown('9')">9</view>
<view class="key-item" @click="keydown('00')">00</view>
<view class="key-item" @click="keydown('0')">0</view>
<view class="key-item" @click="keydown('.')">.</view>
</view>
<view class="action-wrap">
<view class="delete" @click="deleteCode">删除</view>
<view class="delete" @click="paymentMoney = ''">清空</view>
<view class="confirm" @click="paymentMoneyConfirm">确认</view>
</view>
</view>
</view>
</view>
</view>
<uni-popup ref="skuPopup" type="center">
<view class="sku-wrap">
<view class="header">
<text class="title">{{ skuInfo && skuInfo.status == 'edit' ? '调整' : '选择' }}</text>
<text class="iconfont iconguanbi1" @click="$refs.skuPopup.close()"></text>
</view>
<view class="body">
<scroll-view scroll-y="true">
<view class="goods-info" v-if="skuInfo">
<view class="image">
<image v-if="skuInfo.sku_image == '@/static/goods/goods.png'" src="@/static/goods/goods.png" mode="widthFix"/>
<image v-else :src="$util.img(skuInfo.sku_image, { size: 'small' })" @error="skuInfo.sku_image = '@/static/goods/goods.png'" mode="widthFix"/>
</view>
<view class="info">
<view class="multi-hidden">{{ skuInfo.goods_name }}</view>
<view class="price">{{ skuInfo.adjust_price }} / {{ skuInfo.unit ? skuInfo.unit : '件' }}</view>
<view>库存{{ skuInfo.stock }}</view>
</view>
</view>
<view v-for="(item, index) in goodsSpec" :key="index">
<view class="spec">{{ item.spec_name }}</view>
<view class="spec-value">
<view class="value-item" :class="{ active: spec.selected, disabled: (!spec.selected && skuInfo.status == 'edit') }" v-for="(spec, sindex) in item.value" :key="sindex" @click="skuSelect(spec.sku_id)">
{{ spec.spec_value_name }}
</view>
</view>
</view>
<block v-if="skuInfo">
<view class="spec">单价</view>
<view class="spec-value spec-value-form">
<input type="text" class="spec-value-input" v-model="skuInfo.adjust_price" placeholder="请输入单价" />
<text></text>
</view>
<block v-if="skuInfo.goods_class != 6">
<view class="spec">数量</view>
<view class="spec-value spec-value-form">
<view class="num-dec" @click.stop="dec">-</view>
<input type="text" class="spec-value-input" v-model="skuInfo.num" placeholder="请输入数量" :focus="inputFocus" @focus="inputFocus = true" @blur="inputFocus = false" />
<view class="num-inc" @click.stop="inc">+</view>
<text>{{ skuInfo.unit }}</text>
</view>
</block>
<block v-if="skuInfo.goods_class == 6 && skuInfo.pricing_type == 'weight'">
<view class="info">
<view class="spec">剩余库存</view>
<view>{{ skuInfo.stock }}<text>{{ skuInfo.unit }}</text></view>
</view>
<view>
<view class="spec">称重</view>
<view class="spec-value spec-value-form">
<input type="text" class="spec-value-input" v-model="skuInfo.weigh" placeholder="请输入重量" :focus="inputFocus" @focus="inputFocus = true" @blur="inputFocus = false" />
<text>{{ skuInfo.unit }}</text>
</view>
</view>
<view class="flex scale-action" v-if="addon.includes('scale') && cashierScale">
<button type="primary" class="default-btn" plain @click="zero">归零</button>
<button type="primary" class="default-btn" plain @click="tare">去皮</button>
</view>
</block>
</block>
</scroll-view>
</view>
<view class="footer">
<button type="default" class="primary-btn" @click="skuConfirm" :disabled="skuInfo && skuInfo.stock <= 0">确认</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import index from './index.js';
export default {
mixins: [index],
components:{
}
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@@ -0,0 +1,116 @@
<template>
<view class="loading-layer" v-if="isShow" :style="layerBackground">
<view class="loading-anim">
<view class="box item"><view class="border out item color-base-border-top color-base-border-left"></view></view>
</view>
</view>
</template>
<script>
export default {
name: 'nsLoading',
props: {
layerBackground: {
type: Object,
default() {
return {};
}
},
defaultShow: {
type: Boolean,
default: true
}
},
data() {
return {
isShow: true
};
},
created() {
this.isShow = this.defaultShow;
},
methods: {
show() {
this.isShow = true;
},
hide() {
this.isShow = false;
}
}
};
</script>
<style lang="scss" scoped>
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-layer {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 997;
background: #fff;
}
.loading-anim {
position: absolute;
left: 50%;
top: 40%;
transform: translate(-50%, -50%);
}
.loading-anim > .item {
position: relative;
width: 0.3rem;
height: 0.3rem;
perspective: 8rem;
transform-style: preserve-3d;
transition: all 0.2s ease-out;
}
.loading-anim .border {
position: absolute;
border-radius: 50%;
border: 0.03rem solid $primary-color;
}
.loading-anim .out {
top: 15%;
left: 15%;
width: 70%;
height: 70%;
// border-left-color: red !important;
border-right-color: rgba($color: #000000, $alpha: 0) !important;
// border-top-color: rgba($color: #000000, $alpha: 0) !important;
border-bottom-color: rgba($color: #000000, $alpha: 0) !important;
animation: spin 0.6s linear normal infinite;
}
.loading-anim .in {
top: 25%;
left: 25%;
width: 50%;
height: 50%;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
animation: spin 0.8s linear infinite;
}
.loading-anim .mid {
top: 40%;
left: 40%;
width: 20%;
height: 20%;
border-left-color: transparent;
border-right-color: transparent;
animation: spin 0.6s linear infinite;
}
</style>

View File

@@ -0,0 +1,195 @@
import {
getMemberCardList
} from '@/api/member'
import {mapGetters} from 'vuex';
export default {
data() {
return {
memberCardData: {
page: 0,
total: 1,
list: [],
index: 0,
currData: {},
selected: {}
},
itemNum: 1
}
},
computed: {
...mapGetters(['billingGoodsData'])
},
methods: {
open() {
this.$refs.memberCardPopup.open();
this.memberCardData.page = 0;
this.memberCardData.index = 0;
this.memberCardData.list = [];
this.memberCardData.currData = {};
this.memberCardData.selected = {};
this.getMemberCard()
},
// 获取会员项目
getMemberCard() {
if (this.memberCardData.page + 1 > this.memberCardData.total) return;
this.memberCardData.page += 1;
getMemberCardList({
status: 1,
page: this.memberCardData.page,
member_id: this.globalMemberInfo.member_id
}).then(res => {
if (res.code == 0) {
this.memberCardData.total = res.data.page_count || 1;
Object.values(this.billingGoodsData).forEach(data => {
if (data.card_id) {
res.data.list.forEach((card)=>{
if(card.card_id == data.card_id){
// 通用卡:选择商品,总数量发生变化
if (data.card_type == 'commoncard') {
card.total_use_num += data.num;
} else if (data.card_type == 'oncecard') {
// 限次卡:选择商品后,商品和总数量都要发生变化
card.total_use_num += data.num;
card.item_list.forEach((card_item)=>{
if(card_item.item_id == data.item_id){
card_item.use_num += data.num;
}
})
}
}
})
}
});
if (res.data.list.length) this.memberCardData.list = this.memberCardData.list.concat(res.data.list);
if (this.memberCardData.page == 1) {
// 默认展示第一个卡项信息
if (res.data.count) this.selectMemberCard(this.memberCardData.list[0], 0);
}
}
})
},
/**
* 选择会员套餐
* @param {Object} data
* @param {Object} index
*/
selectMemberCard(data, index) {
this.memberCardData.index = index;
this.memberCardData.currData = this.$util.deepClone(data);
this.memberCardData.selected = {};
},
/**
* 选择会员套餐商品项
* @param {Object} data
* @param {Object} index
*/
selectMemberCardItem(data, index) {
if (this.memberCardData.selected['item_' + data.item_id]) {
if (data.card_type == 'commoncard') {
this.memberCardData.currData.total_use_num -= this.memberCardData.selected['item_' + data.item_id].input_num;
}
delete this.memberCardData.selected['item_' + data.item_id];
} else {
if (!this.checkStatus(data)) return;
this.memberCardData.selected['item_' + data.item_id] = this.$util.deepClone(data);
this.memberCardData.selected['item_' + data.item_id].input_num = 1;
this.memberCardData.selected['item_' + data.item_id].index = index;
this.memberCardData.selected['item_' + data.item_id].card_name = this.memberCardData.currData.goods_name;
if (data.card_type == 'commoncard') {
this.memberCardData.currData.total_use_num += 1;
}
}
this.$forceUpdate();
},
/**
* 加入购物车
*/
selectGoods() {
if (!Object.keys(this.memberCardData.selected).length) {
this.$util.showToast({
title: '请选择服务/商品',
});
return;
}
let billingGoodsData = this.$util.deepClone(this.billingGoodsData);
let billingGoodsKeys = Object.keys(billingGoodsData);
Object.keys(this.memberCardData.selected).forEach((key) => {
let data = this.memberCardData.selected[key];
data.card_index = this.memberCardData.index;
this.memberCardData.list[this.memberCardData.index].total_use_num += data.input_num;
this.memberCardData.list[this.memberCardData.index].item_list[data.index].use_num += data.input_num;
this.memberCardData.currData.item_list[data.index].use_num += data.input_num;
//服务商品每个都是一个订单项,需要循环处理
if(data.goods_class == this.$util.goodsClassDict.service){
let addNum = 0;
Object.values(billingGoodsData).forEach((item)=>{
if(item.sku_id == data.sku_id){
addNum ++;
}
})
data.num = 1;
for(let num = 1;num <= data.input_num;num ++){
let skuKey = 'sku_' + data.sku_id + '_item_' + data.item_id + '_' + addNum;
billingGoodsData[skuKey] = this.$util.deepClone(data);
addNum ++;
}
}else{
data.num = data.input_num;
let skuKey = 'sku_' + data.sku_id + '_item_' + data.item_id;
if(billingGoodsData.hasOwnProperty(skuKey)){
data.num += billingGoodsData[skuKey].num;
}
billingGoodsData[skuKey] = this.$util.deepClone(data);
}
});
this.$store.commit('billing/setGoodsData', billingGoodsData);
this.memberCardData.selected = {};
},
/**
* 数量减
* @param {Object} data
*/
itemDec(data) {
let currData = this.memberCardData.currData;
if (this.memberCardData.selected['item_' + data.item_id].input_num > 1) {
this.memberCardData.selected['item_' + data.item_id].input_num -= 1;
if (data.card_type == 'commoncard') {
currData.total_use_num -= 1;
}
this.$forceUpdate();
}
},
/**
* 数量加
* @param {Object} data
*/
itemInc(data) {
let currData = this.memberCardData.currData;
if (data.card_type == 'commoncard') {
if ((currData.total_num - currData.total_use_num - 1) < 0) return;
} else if (data.card_type == 'oncecard') {
if ((data.num - data.use_num - this.memberCardData.selected['item_' + data.item_id].input_num - 1) < 0) return;
}
if (data.card_type == 'commoncard') {
currData.total_use_num += 1;
}
this.memberCardData.selected['item_' + data.item_id].input_num += 1;
this.$forceUpdate();
},
checkStatus(data) {
let currData = this.memberCardData.currData;
if (data.card_type == 'commoncard') {
return currData.total_num > currData.total_use_num;
} else if (data.card_type == 'oncecard') {
return data.num > data.use_num;
}
return true;
}
}
}

View File

@@ -0,0 +1,376 @@
.container {
height: 100%;
}
.header {
height: 0.66rem;
line-height: 0.66rem;
text-align: left;
border-bottom: 0.01rem solid #e6e6e6;
color: #303133;
font-size: 0.14rem;
}
.info-wrap {
display: flex;
flex-direction: column;
height: 6.5rem;
padding: 0 0.2rem;
box-sizing: border-box;
.headimg-content {
display: flex;
align-items: center;
margin-top: 0.2rem;
.headimg {
width: 0.7rem;
height: 0.7rem;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.header-info {
margin-left: 0.15rem;
width: calc(100% - 0.85rem);
.name {
font-size: 0.16rem;
color: #303133;
text {
background: #ffffff;
border: 0.01rem solid $primary-color;
border-radius: 0.02rem;
font-size: 0.12rem;
color: $primary-color;
margin-left: 0.15rem;
padding: 0.01rem 0.04rem;
}
}
.header-info-item {
display: flex;
align-items: center;
margin-top: 0.1rem;
justify-content: space-between;
view {
text-align: left;
font-size: 0.14rem;
color: #303133;
opacity: 0.9;
}
}
}
}
}
.empty {
text-align: center;
padding-top: 1.2rem;
margin: 0 auto;
image {
width: 2rem;
}
.tips {
color: #999;
margin-top: 0.15rem;
}
}
.member-card-wrap {
flex: 1;
height: 0;
display: flex;
flex-direction: column;
.card-wrap {
flex: 1;
height: 0;
display: flex;
padding-top: 0.2rem;
margin-bottom: 0.2rem;
}
.card-list {
width: 2rem;
border: 0.01rem solid #e6e6e6;
margin-right: 0.1rem;
padding: 0.1rem 0;
.card-item {
width: calc(100% - 0.2rem);
height: 1rem;
border: 0.01rem solid $primary-color;
margin: 0 0.1rem 0.1rem 0.1rem;
box-sizing: border-box;
border-radius: 0.05rem;
cursor: pointer;
padding: 0.15rem 0.1rem;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: var(--primary-color-light-8);
&.active {
background-color: $primary-color;
color: #fff;
.card-name {
color: #fff;
}
.info {
color: #fff;
}
}
.card-name {
font-weight: bold;
}
.info {
display: flex;
justify-content: space-between;
color: #999;
& > view {
font-size: 0.12rem;
}
}
}
}
.item-list {
flex: 1;
border: 0.01rem solid #e6e6e6;
display: flex;
flex-direction: column;
width: 0;
.content {
padding: 0 0.1rem;
}
.empty {
padding-top: 0.8rem;
}
.title {
line-height: 0.3rem;
padding: 0.1rem;
display: flex;
justify-content: space-between;
.num {
color: $primary-color;
margin: 0 0.02rem;
}
}
.button-wrap {
display: flex;
background-color: #fff;
height: 0.5rem;
line-height: 0.5rem;
align-items: center;
justify-content: flex-end;
box-shadow: 0 0.04rem 0.12rem 0 rgba(0, 0, 0, 0.1);
padding: 0.1rem 0;
button {
height: 0.4rem;
line-height: 0.4rem;
margin: 0 0.1rem 0 0;
}
}
.item-wrap {
flex: 1;
height: 0;
display: flex;
.uni-flex {
flex-wrap: wrap;
}
}
.card-item {
display: flex;
width: calc(50% - 0.05rem);
padding: 0.1rem;
border: 0.01rem solid #eee;
border-radius: 0.03rem;
cursor: pointer;
transition: all 0.3s;
box-sizing: border-box;
margin-bottom: 0.1rem;
.image {
width: 0.7rem;
height: 0.7rem;
margin-right: 0.1rem;
overflow: hidden;
image {
width: 100%;
}
}
.info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 0;
.num {
margin-top: 0.05rem;
color: #999;
font-size: 0.12rem;
}
.price {
font-size: 0.14rem;
color: #fe2278;
line-height: 1;
.util {
font-size: 0.12rem;
}
}
.name {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.5;
.tag {
border-radius: 0.02rem;
padding: 0.01rem 0.05rem;
background-color: var(--primary-color-light-8);
color: $primary-color;
font-size: 0.12rem;
margin-right: 0.05rem;
}
}
}
}
.action-wrap {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.12rem;
margin-top: 0.05rem;
height: 0.25rem;
}
.number-wrap {
display: none;
height: 0.25rem;
border: 0.01rem solid #e6e6e6;
border-radius: 0.02rem;
overflow: hidden;
input {
height: 0.25rem;
line-height: 0.25rem;
width: 0.25rem;
border: 1px solid #e6e6e6;
text-align: center;
background: #fff;
font-size: 0.12rem;
}
.iconfont {
height: 0.25rem;
width: 0.25rem;
text-align: center;
line-height: 0.25rem;
background: #f5f5f5;
}
}
.card-item.active {
background-color: var(--primary-color-light-2);
.num {
color: #fff;
}
.price {
color: #fff;
}
.name {
color: #fff;
.tag {
background-color: #fff;
}
}
.number-wrap {
display: flex;
}
}
.not-select {
background: #eee;
cursor: not-allowed;
}
}
}
// pop弹框
.pop-box {
background: #ffffff;
width: 8rem;
height: 7rem;
.pop-header {
padding: 0 0.15rem 0 0.2rem;
height: 0.5rem;
line-height: 0.5rem;
border-bottom: 0.01rem solid #f0f0f0;
font-size: 0.14rem;
color: #333;
overflow: hidden;
border-radius: 0.02rem 0.2rem 0 0;
box-sizing: border-box;
display: flex;
justify-content: space-between;
.pop-header-text {
}
.pop-header-close {
cursor: pointer;
text {
font-size: 0.18rem;
}
}
}
.pop-content {
height: calc(100% - 1rem);
overflow-y: scroll;
padding: 0.1rem 0.2rem;
box-sizing: border-box;
}
}

View File

@@ -0,0 +1,129 @@
<template>
<view class="container">
<uni-popup ref="memberCardPopup">
<view class="pop-box member-info-wrap">
<view class="pop-header">
<view class="pop-header-text">会员卡项</view>
<view class="pop-header-close" @click="$refs.memberCardPopup.close()">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<view class="info-wrap" v-if="globalMemberInfo">
<view class="headimg-content">
<view class="headimg">
<image :src="globalMemberInfo.headimg ? $util.img(globalMemberInfo.headimg) : $util.img(defaultImg.head)" @error="globalMemberInfo.headimg = defaultImg.head"/>
</view>
<view class="header-info">
<view class="name">
{{ globalMemberInfo.nickname }}
<text v-if="globalMemberInfo.member_level">{{ globalMemberInfo.member_level_name }}</text>
</view>
<view class="header-info-item">
<view>电话{{ globalMemberInfo.mobile }}</view>
<view>性别{{ globalMemberInfo.sex == 0 ? '未知' : globalMemberInfo.sex == 1 ? '男' : '女' }}</view>
<view>生日{{ globalMemberInfo.birthday }}</view>
<view>注册时间{{ globalMemberInfo.reg_time | timeFormat }}</view>
</view>
</view>
</view>
<view class="member-card-wrap">
<view class="card-wrap">
<scroll-view scroll-y="true" class="card-list" @scrolltolower="getMemberCard()">
<block v-if="memberCardData.list.length">
<view class="card-item" :class="{ active: memberCardData.index == index }" v-for="(item, index) in memberCardData.list" :key="index" @click="selectMemberCard(item, index)">
<view class="card-name">{{ item.goods_name }}</view>
<view class="info">
<view v-if="item.total_num > 0">可用{{ item.total_num - item.total_use_num }}</view>
<view v-else>不限次</view>
<view v-if="item.end_time > 0">{{ $util.timeFormat(item.end_time, 'Y/m/d') }}</view>
<view v-else>长期有效</view>
</view>
</view>
</block>
<view v-else class="empty">
<image src="@/static/card/card_empty.png" mode="widthFix"/>
<view class="tips">暂无可用卡项</view>
</view>
</scroll-view>
<view class="item-list">
<view class="title">
<view>可用服务/商品</view>
<view v-if="memberCardData.currData.card_type == 'commoncard'">
<text>以下服务/商品剩余可用</text>
<text class="num">{{ memberCardData.currData.total_num - memberCardData.currData.total_use_num }}</text>
<text></text>
</view>
</view>
<scroll-view scroll-y="true" class="item-wrap">
<view class="uni-flex justify-between content" v-if="memberCardData.currData.item_list">
<view class="card-item" :class="{
active: memberCardData.selected['item_' + item.item_id],
'not-select': !checkStatus(item) && !memberCardData.selected['item_' + item.item_id]
}" @click="selectMemberCardItem(item, index)" v-for="(item, index) in memberCardData.currData.item_list">
<view class="image">
<image v-if="item.sku_image == '@/static/goods/goods.png'" src="@/static/goods/goods.png" mode="widthFix"/>
<image v-else :src="$util.img(item.sku_image.split(',')[0], { size: 'small' })" @error="item.sku_image = '@/static/goods/goods.png'" mode="widthFix"/>
</view>
<view class="info">
<view>
<view class="name">
<text class="tag">{{ item.is_virtual ? '服务' : '商品' }}</text>
<text>{{ item.sku_name }}</text>
</view>
<block v-if="memberCardData.currData.card_type != 'commoncard'">
<view class="num" v-if="item.num > 0">剩余可用{{ item.num - item.use_num }}</view>
<view class="num" v-else>不限次</view>
</block>
</view>
<view class="action-wrap">
<view class="price">
<text class="util"></text>
{{ item.price }}
</view>
<view class="number-wrap" v-if="memberCardData.selected['item_' + item.item_id]">
<text class="iconfont iconjian" @click.stop="itemDec(memberCardData.selected['item_' + item.item_id])"></text>
<input type="number" v-model="memberCardData.selected['item_' + item.item_id].input_num" />
<text class="iconfont iconjia" @click.stop="itemInc(memberCardData.selected['item_' + item.item_id])"></text>
</view>
</view>
</view>
</view>
</view>
<view class="empty" v-else>
<image src="@/static/goods/goods_empty.png" mode="widthFix"/>
<view class="tips">暂无相关数据</view>
</view>
</scroll-view>
<view class="button-wrap">
<button type="default" class="primary-btn" :disabled="memberCardData.itemIndex == -1" @click="selectGoods()">加入购物车</button>
</view>
</view>
</view>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import uniDataCheckbox from '@/components/uni-data-checkbox/uni-data-checkbox.vue';
import UniPopup from "../uni-popup/uni-popup";
import index from './index.js';
export default {
name: 'nsMember',
components: {
UniPopup,
uniDataCheckbox
},
mixins: [index]
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@@ -0,0 +1,398 @@
<template>
<view class="member-detail-wrap">
<!-- 卡包 -->
<uni-popup ref="cardListPop">
<view class="pop-box card-list-pop-box">
<view class="pop-header">
<view class="pop-header-text">卡包</view>
<view class="pop-header-close" @click="close('cardlist')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<dataTable url="/cardservice/storeapi/membercard/lists" :cols="card" ref="table" :option="option" :pagesize="pageSize">
<template v-slot:action="dataTable">
<text class="view-detail" @click="viewDetails(dataTable.value.card_id)">查看详情</text>
</template>
</dataTable>
</scroll-view>
</view>
</uni-popup>
<uni-popup ref="cardDetailPop">
<view class="pop-box cardDetailPop-box">
<view class="pop-header">
<view class="pop-header-text">详情</view>
<view class="pop-header-close" @click="$refs.cardDetailPop.close()">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<view class="pop-content">
<view class="tab-head">
<text v-for="(item, index) in tabObj.list" :key="index" :class="{ active: tabObj.index == item.value }" v-if="(item.value == 3 && card_detail.card_log && card_detail.card_log.length > 0) || item.value != 3" @click="tabObj.index = item.value">
{{ item.name }}
</text>
</view>
<view class="tab-content">
<view class="basic-info" v-if="tabObj.index == 0">
<view class="basic-item using-hidden">卡项名称{{ basicInfo.goods_name }}</view>
<view class="basic-item">价格{{ basicInfo.price }}</view>
<view class="basic-item">
卡类型{{ (basicInfo.card_type == 'oncecard' && '限次卡') || (basicInfo.card_type == 'timecard' && '限时卡') || (basicInfo.card_type == 'commoncard' && '通用卡') }}
</view>
<view class="basic-item">总次数/已使用{{ basicInfo.card_type == 'timecard' ? '不限' : basicInfo.total_num }}/{{ basicInfo.total_use_num }}</view>
<view class="basic-item">获取时间{{ $util.timeFormat(basicInfo.create_time) }}</view>
<view class="basic-item">到期时间{{ basicInfo.end_time > 0 ? $util.timeFormat(basicInfo.end_time) : '永久有效' }}</view>
</view>
<view class="other-information" v-if="tabObj.index == 1 && basicInfo && basicInfo.card_item">
<view class="information-head">
<text>商品名称</text>
<text>总次数/已使用</text>
<text>有效期</text>
</view>
<view class="information-body">
<view class="information-tr" v-for="(item, index) in basicInfo.card_item" :key="index">
<text class="using-hidden">{{ item.sku_name }}</text>
<text>{{ item.card_type == 'timecard' ? '不限' : item.num }} /{{ item.use_num }}</text>
<text>{{ item.end_time > 0 ? $util.timeFormat(item.end_time) : '永久有效' }}</text>
</view>
<view class="information-tr empty" v-if="!basicInfo.card_item.length">
<view class="iconfont iconwushuju"></view>
<view>暂无数据</view>
</view>
</view>
</view>
<view class="card-info" v-if="tabObj.index == 2">
<dataTable url="/cardservice/storeapi/membercard/records" :cols="cardInfo.card" ref="table" :option="cardInfo.option" :pagesize="cardInfo.pageSize"></dataTable>
</view>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { getMemberCardDetail } from '@/api/member'
import dataTable from '@/components/uni-data-table/uni-data-table.vue';
export default {
components: {
dataTable
},
props: {
option: {}
},
data() {
return {
pageSize: 8,
card: [{
width: 20,
title: '名称',
align: 'left',
field: 'goods_name'
}, {
width: 18,
title: '卡号',
align: 'center',
field: 'card_code'
}, {
width: 8,
title: '卡类型',
align: 'left',
templet: function (data) {
if (data.card_type == 'oncecard') return '限次卡';
if (data.card_type == 'timecard') return '限时卡';
if (data.card_type == 'commoncard') return '通用卡';
}
}, {
width: 12,
title: '总次数/已使用',
align: 'center',
templet: data => {
var totalNum = data.card_type == 'timecard' ? '不限' : data.total_num;
return totalNum + '/' + data.total_use_num;
}
}, {
width: 17,
title: '创建时间',
align: 'center',
templet: data => {
return this.$util.timeFormat(data.create_time);
}
}, {
width: 17,
title: '到期时间',
align: 'center',
templet: data => {
if (data.end_time) return this.$util.timeFormat(data.end_time);
else return '长期有效';
}
}, {
width: 8,
title: '操作',
align: 'right',
action: true
}],
tabObj: {
list: [{
value: 0,
name: '基础信息'
}, {
value: 1,
name: '商品/项目'
}, {
value: 2,
name: '使用记录'
}],
index: 1
},
currCardId: 0,
basicInfo: {},
cardInfo: {
card: [{
width: 40,
title: '卡项名称',
align: 'left',
field: 'sku_name'
}, {
width: 20,
title: '使用次数',
align: 'center',
field: 'num'
}, {
width: 25,
title: '使用时间',
align: 'right',
templet: data => {
return this.$util.timeFormat(data.create_time);
}
}, {
width: 15,
title: '操作',
align: 'right',
action: true
}],
option: {},
pageSize: 6
}
};
},
created() { },
methods: {
open() {
this.$refs.cardListPop.open();
},
close() {
this.$refs.cardListPop.close();
},
viewDetails(card_id) {
this.currCardId = card_id;
this.$refs.cardDetailPop.open();
this.getCardDetail();
this.cardInfo.option.member_id = this.globalMemberInfo.member_id;
this.cardInfo.option.card_id = this.currCardId;
},
getCardDetail() {
let data = {};
data.member_id = this.globalMemberInfo.member_id;
data.card_id = this.currCardId;
getMemberCardDetail(data).then(res => {
this.basicInfo = {};
if (res.code >= 0) {
this.basicInfo = res.data;
}
});
}
}
};
</script>
<style lang="scss" scoped>
.pop-box {
background: #ffffff;
width: 8rem;
height: 7rem;
.pop-header {
padding: 0 0.15rem 0 0.2rem;
height: 0.5rem;
line-height: 0.5rem;
border-bottom: 0.01rem solid #f0f0f0;
font-size: 0.14rem;
color: #333;
overflow: hidden;
border-radius: 0.02rem 0.2rem 0 0;
box-sizing: border-box;
display: flex;
justify-content: space-between;
.pop-header-close {
cursor: pointer;
text {
font-size: 0.18rem;
}
}
}
.pop-content {
height: calc(100% - 1rem);
overflow-y: scroll;
padding: 0.1rem 0.2rem;
box-sizing: border-box;
}
.pop-bottom {
button {
width: 95%;
}
}
}
.card-list-pop-box {
width: 10rem;
height: 5.7rem;
.pop-content {
height: calc(100% - 0.5rem);
}
/deep/ .tpage {
position: absolute;
right: 0;
bottom: 0;
}
.basic-box {
display: flex;
justify-content: space-between;
margin-bottom: 0.2rem;
padding: 0.2rem;
box-sizing: border-box;
}
.basic {
padding: 0.1rem;
margin-bottom: 0.5rem;
}
}
.cardDetailPop-box {
width: 10rem;
height: 5.7rem;
.tab-head {
display: flex;
background-color: #f7f8fa;
text {
height: 0.5rem;
line-height: 0.5rem;
text-align: center;
padding: 0 0.35rem;
box-sizing: border-box;
&.active {
background-color: #fff;
}
}
}
.pop-content {
overflow-y: inherit;
.basic-info {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0.2rem;
.basic-item {
flex-basis: 33%;
height: 0.4rem;
line-height: 0.4rem;
}
}
}
.other-information {
display: flex;
justify-content: space-between;
flex-direction: column;
padding-top: 0.2rem;
.information-head {
display: flex;
justify-content: space-between;
background-color: #f7f8fa;
text {
padding: 0 0.2rem;
height: 0.5rem;
line-height: 0.5rem;
&:nth-child(1) {
flex-basis: 35%;
}
&:nth-child(2) {
flex-basis: 35%;
}
&:nth-child(2) {
flex-basis: 30%;
}
}
}
.information-tr {
display: flex;
justify-content: space-between;
border-bottom: 0.01rem solid #e6e6e6;
text {
padding: 0 0.2rem;
height: 0.5rem;
line-height: 0.5rem;
&:nth-child(1) {
flex-basis: 35%;
}
&:nth-child(2) {
flex-basis: 35%;
}
&:nth-child(2) {
flex-basis: 30%;
}
}
&.empty {
display: flex;
justify-content: center;
align-items: center;
height: 0.5rem;
color: #909399;
.iconfont {
font-size: 0.25rem;
margin: 0.05rem;
}
}
}
}
.card-info {
display: flex;
justify-content: space-between;
padding-top: 0.2rem;
}
}
.view-detail {
color: $primary-color;
}</style>

View File

@@ -0,0 +1,183 @@
import {
getMemberInfoById,
getMemberLevelList,
getCouponTypeList,
sendMemberCoupon,
applyingMembershipCard
} from '@/api/member'
export default {
data() {
return {
pageSize: 8,
sex: [{
text: '未知',
value: 0
}, {
text: '男',
value: 1
}, {
text: '女',
value: 2
}],
sendCoupon: {
list: [],
page: 1
},
memberLevelList: [],
applyMember: {
level_id: '',
member_level: '',
member_level_name: '',
member_code: ''
}
};
},
created() {
this.getMemberLevel();
},
methods: {
open() {
this.getMemberInfo(); // 保证数据实时性
this.$refs.memberPop.open();
},
getMemberInfo() {
getMemberInfoById(this.globalMemberInfo.member_id).then(res => {
if (res.code >= 0) {
res.data.birthday = res.data.birthday > 0 ? this.$util.timeFormat(res.data.birthday, 'Y-m-d') : '--';
this.$store.commit('app/setGlobalMemberInfo', res.data);
}
});
},
getMemberLevel() {
this.memberLevelList = [];
getMemberLevelList().then(res => {
if (res.code == 0 && res.data) {
for (let i in res.data) {
this.memberLevelList.push({
label: res.data[i]['level_name'],
value: res.data[i]['level_id'].toString(),
disabled: false
});
}
}
});
},
selectMemberLevel(index, item) {
if (index >= 0) {
this.applyMember.level_id = item.value;
this.applyMember.member_level = item.value;
this.applyMember.member_level_name = item.label;
} else {
this.applyMember.level_id = '';
this.applyMember.member_level = item.value;
this.applyMember.member_level_name = item.label;
}
},
// 客户操作
memberAction(type) {
switch (type) {
case 'sendCoupon':
this.getCouponList();
this.$refs.sendCouponPop.open('center');
break;
case 'applyMember':
this.$refs.applyMemberPop.open();
break;
}
},
popClose(type) {
this.$refs[type + 'Pop'].close();
},
//获取发放优惠券列表
getCouponList() {
let data = {
page: this.sendCoupon.page,
page_size: 7
};
getCouponTypeList(data).then(res => {
if (res.code >= 0) {
if (this.sendCoupon.page == 1) this.sendCoupon.list = [];
if (res.data.list && res.data.list.length) {
res.data.list.forEach((item, index) => {
if (item.validity_type == 0) item.validity_name = '失效日期:' + this.$util.timeFormat(item.end_time);
else if (item.validity_type == 1) item.validity_name = '领取后,' + item.fixed_term + '天有效';
else item.validity_name = '长期有效';
item.num = 0;
});
}
this.sendCoupon.list = this.sendCoupon.list.concat(res.data.list);
if (res.data.page_count >= this.sendCoupon.page) this.sendCoupon.page++;
}
});
},
// 发放数量
dec: function (item) {
if (item.num > 0) {
item.num = item.num - 1;
}
},
inc: function (item) {
item.num = item.num + 1;
},
// 发放优惠券
sendCouponFn() {
if (!this.sendCoupon.list || !this.sendCoupon.list.length) return false;
let data = {};
data.member_id = this.globalMemberInfo.member_id;
data.coupon_data = '';
let couponDataArr = [];
this.sendCoupon.list.forEach((item, index) => {
if (item.num > 0) {
let obj = {};
obj.coupon_type_id = item.coupon_type_id;
obj.num = item.num;
couponDataArr.push(obj);
}
});
if (couponDataArr.length <= 0) return false;
data.coupon_data = JSON.stringify(couponDataArr);
sendMemberCoupon(data).then(res => {
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.sendCoupon.page = 1;
this.sendCoupon.list = [];
this.getMemberInfo();
this.$refs.sendCouponPop.close();
}
});
},
//打开会员卡项
showMemberCard() {
this.$refs.memberCardPopup.open();
},
// 办理会员卡
saveApplyMember() {
if (!this.applyMember.level_id) {
this.$util.showToast({
title: '请选择会员卡等级'
});
return false;
}
applyingMembershipCard({
member_id: this.globalMemberInfo.member_id,
level_id: this.applyMember.level_id,
member_code: this.applyMember.member_code
}).then(res => {
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.getMemberInfo();
this.popClose('applyMember');
}
});
},
headError(item) {
item.headimg = this.defaultImg.head;
}
}
}

View File

@@ -0,0 +1,412 @@
.member-detail-wrap {
width: 100%;
border-left: 0;
.member-head {
height: 0.66rem;
line-height: 0.66rem;
box-sizing: border-box;
border-bottom: 0.01rem solid #e6e6e6;
font-size: 0.14rem;
}
.member-content {
padding: 0.15rem;
width: 100%;
height: calc(100vh - 0.8rem);
box-sizing: border-box;
.content-block {
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
.item-img {
width: 0.7rem;
height: 0.7rem;
border-radius: 50%;
box-sizing: border-box;
image {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.item-content {
padding-left: 0.15rem;
width: calc(100% - 0.7rem);
box-sizing: border-box;
.item-title {
width: 100%;
font-size: 0.16rem;
align-items: center;
display: flex;
.item-label {
border: 0.01rem solid $primary-color;
color: $primary-color;
background-color: #fff;
border-radius: 0.02rem;
width: fit-content;
padding: 0.01rem 0.05rem;
margin-left: 0.15rem;
}
.item-title-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 50%;
font-size: 0.16rem;
}
}
.info-list {
margin-top: 0.1rem;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.info-item {
font-size: .14rem;
padding-right: .2rem;
width: 50%;
box-sizing: border-box;
height: .25rem;
line-height: .25rem;
}
}
}
}
.content-block.account {
border: 0.01rem solid #e6e6e6;
background-color: #ffffff;
display: flex;
flex-wrap: wrap;
margin-top: 0.2rem;
border-radius: 0.03rem;
align-items: baseline;
padding: .1rem 0;
.content-data-item {
padding: .1rem 0;
width: 33%;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.data-item-title {
}
.data-item-value {
font-size: 0.26rem;
margin-top: 0.1rem;
}
}
}
.content-block.assets {
display: flex;
justify-content: space-around;
margin-top: 0.2rem;
.content-data-left {
background-color: #ffffff;
padding: 0.25rem 0;
border-radius: 0.03rem;
width: calc(50% - 0.075rem);
margin-right: 0.15rem;
display: flex;
justify-content: space-around;
height: 1rem;
.content-data-item {
.data-item-title {
}
.data-item-value {
font-size: 0.26rem;
margin-top: 0.1rem;
}
}
}
}
.content-block.action {
display: flex;
justify-content: flex-start;
margin-top: 0.2rem;
.content-data-item {
border: 0.01rem solid #e6e6e6;
width: calc(100% / 3);
background-color: #ffffff;
display: flex;
padding: 0.15rem 0;
border-radius: 0.03rem;
align-items: center;
text-align: center;
flex-direction: column;
margin-right: 0.15rem;
cursor: pointer;
.data-item-icon {
width: 0.55rem;
height: 0.55rem;
image {
width: 100%;
height: 100%;
}
}
.data-item-value {
margin-top: 0.1rem;
}
&:last-child {
margin-right: 0;
}
}
}
}
}
// pop弹框
.pop-box {
background: #ffffff;
width: 8rem;
height: 7rem;
.pop-header {
padding: 0 0.15rem 0 0.2rem;
height: 0.5rem;
line-height: 0.5rem;
border-bottom: 0.01rem solid #f0f0f0;
font-size: 0.14rem;
color: #333;
overflow: hidden;
border-radius: 0.02rem 0.2rem 0 0;
box-sizing: border-box;
display: flex;
justify-content: space-between;
.pop-header-text {
}
.pop-header-close {
cursor: pointer;
text {
font-size: 0.18rem;
}
}
}
.pop-content {
height: calc(100% - 1rem);
overflow-y: scroll;
padding: 0.1rem 0.2rem;
box-sizing: border-box;
}
.pop-bottom {
button {
width: 95%;
}
}
}
//表单
.form-content {
display: flex;
flex-direction: column;
align-items: center;
.form-item {
margin-bottom: 0.1rem;
display: flex;
&:last-of-type {
margin-bottom: 0;
}
.form-label {
width: 1.2rem;
text-align: right;
padding-right: 0.1rem;
box-sizing: border-box;
height: 0.32rem;
line-height: 0.32rem;
.required {
color: red;
margin-right: 0.03rem;
}
}
.form-inline {
width: 2.4rem;
line-height: 0.32rem;
margin-right: 0.1rem;
box-sizing: border-box;
.form-input {
border-width: 0.01rem;
border-style: solid;
background-color: #fff;
color: rgba(0, 0, 0, 0.85);
border-radius: 0.02rem;
padding-left: 0.1rem;
height: 0.32rem;
line-height: 0.32rem;
font-size: 0.14rem;
border-color: #e6e6e6;
}
.word-aux {
color: #999;
font-size: 0.12rem;
line-height: 1.5;
margin-top: 0.05rem;
}
}
}
}
.member-info-wrap {
width: 5.5rem;
height: 5.2rem;
}
.applyMemberPop-box {
width: 6rem;
height: 3.38rem;
.pop-content {
overflow: initial;
}
}
.sendCoupon-box {
width: 9rem;
height: 5.06rem;
.sendCoupon-content {
padding: 0.1rem 0.2rem;
.coupon-table-head {
display: flex;
background: #f7f8fa;
}
.coupon-table-body {
height: 3.2rem;
.coupon-table-tr {
display: flex;
border-bottom: 0.01rem solid #e6e6e6;
}
.table-input {
height: 0.3rem;
line-height: 0.3rem;
border: 0.01rem solid #e6e6e6;
padding: 0 0.1rem;
text-align: center;
}
.item-num {
display: flex;
align-items: center;
margin-left: 0.1rem;
.num-dec {
width: 0.6rem;
height: 0.25rem;
background: #e6e6e6;
border: 0.01rem solid #e6e6e6;
border-radius: 30%;
text-align: center;
line-height: 0.23rem;
font-size: 0.25rem;
margin-right: 0.1rem;
cursor: pointer;
transition: 0.3s;
}
.num-inc {
width: 0.6rem;
height: 0.25rem;
background: $primary-color;
border: 0.01rem solid #e6e6e6;
border-radius: 30%;
text-align: center;
line-height: 0.23rem;
font-size: 0.25rem;
margin-left: 0.1rem;
cursor: pointer;
transition: 0.3s;
color: #fff;
}
}
.coupon-table-td:nth-child(4) {
padding: 0 0.05rem;
}
}
.coupon-table-td,
.coupon-table-th {
padding: 0 0.1rem;
display: flex;
align-items: center;
height: 0.5rem;
&:nth-child(1) {
flex-basis: 30%;
}
&:nth-child(2) {
flex-basis: 20%;
}
&:nth-child(3) {
flex-basis: 30%;
}
&:nth-child(4) {
justify-content: flex-end;
flex-basis: 20%;
text-align: right;
}
}
}
.pop-bottom {
margin-top: 0.12rem;
}
.empty {
display: flex;
align-items: center;
justify-content: center;
height: 0.5rem;
border-bottom: 0.01rem solid #e6e6e6;
color: #909399;
.iconfont {
font-size: 0.25rem;
margin: 0.05rem;
}
}
}

View File

@@ -0,0 +1,202 @@
<template>
<view class="member-detail-wrap">
<uni-popup ref="memberPop">
<view class="pop-box member-info-wrap" v-if="globalMemberInfo">
<view class="pop-header">
<view class="pop-header-text">会员详情</view>
<view class="pop-header-close" @click="popClose('member')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<view class="member-content">
<view class="content-block">
<view class="item-img">
<image mode="aspectFill" v-if="globalMemberInfo.headimg" :src="$util.img(globalMemberInfo.headimg)" @error="headError(globalMemberInfo)"/>
<image mode="aspectFill" v-else :src="$util.img(defaultImg.head)"/>
</view>
<view class="item-content">
<view class="item-title">
<view class="item-title-text">{{ globalMemberInfo.nickname ? globalMemberInfo.nickname : '' }}</view>
<view class="item-label" v-if="globalMemberInfo.member_level && globalMemberInfo.member_level_name">{{ globalMemberInfo.member_level_name }}</view>
</view>
<view class="info-list">
<view class="info-item">手机{{ globalMemberInfo.mobile ? globalMemberInfo.mobile : '' }}</view>
<view class="info-item" v-if="globalMemberInfo.sex == 0">性别未知</view>
<view class="info-item" v-if="globalMemberInfo.sex == 1">性别</view>
<view class="info-item" v-if="globalMemberInfo.sex == 2">性别</view>
<view class="info-item">生日{{ globalMemberInfo.birthday }}</view>
<view class="info-item" v-if="globalMemberInfo.member_time">成为会员{{ $util.timeFormat(globalMemberInfo.member_time,'Y-m-d') }}</view>
</view>
</view>
</view>
<view class="content-block account">
<view class="content-data-item">
<view class="data-item-title">积分</view>
<view class="data-item-value">{{ globalMemberInfo.point ? parseInt(globalMemberInfo.point) : '0' }}</view>
</view>
<view class="content-data-item">
<view class="data-item-title">储值余额()</view>
<view class="data-item-value">{{ globalMemberInfo.balance ? globalMemberInfo.balance : '0.00' }}</view>
</view>
<view class="content-data-item">
<view class="data-item-title">现金余额()</view>
<view class="data-item-value">{{ globalMemberInfo.balance_money ? globalMemberInfo.balance_money : '0.00' }}</view>
</view>
<view class="content-data-item">
<view class="data-item-title">成长值</view>
<view class="data-item-value">{{ globalMemberInfo.growth ? globalMemberInfo.growth : '0' }}</view>
</view>
<view class="content-data-item">
<view class="data-item-title">优惠券()</view>
<view class="data-item-value">{{ globalMemberInfo.coupon_num ? globalMemberInfo.coupon_num : '0' }}</view>
</view>
<view class="content-data-item">
<view class="data-item-title">卡包</view>
<view class="data-item-value">{{ globalMemberInfo.card_num ? globalMemberInfo.card_num : '0' }}
</view>
</view>
</view>
<view class="content-block action">
<view class="content-data-item" @click="memberAction('sendCoupon')">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-coupon.png" />
</view>
<view class="data-item-value">送优惠券</view>
</view>
<view class="content-data-item" v-if="isShowMemberCard" @click="showMemberCard">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-balance.png" />
</view>
<view class="data-item-value">会员卡项</view>
</view>
<view class="content-data-item" @click="memberAction('applyMember')" v-if="!globalMemberInfo.is_member">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-apply.png" />
</view>
<view class="data-item-value">办理会员</view>
</view>
</view>
</view>
</view>
</uni-popup>
<!-- 发放优惠券 -->
<uni-popup ref="sendCouponPop">
<view class="pop-box sendCoupon-box">
<view class="pop-header">
<view class="pop-header-text">送优惠券</view>
<view class="pop-header-close" @click="popClose('sendCoupon')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<view class="common-scrollbar sendCoupon-content">
<view class="coupon-table-head">
<view class="coupon-table-th">优惠券名称</view>
<view class="coupon-table-th">金额</view>
<view class="coupon-table-th">有效期</view>
<view class="coupon-table-th">发放数量</view>
</view>
<scroll-view class="coupon-table-body" @scrolltolower="getCouponList()" scroll-y="true">
<view class="coupon-table-tr" v-for="(item, index) in sendCoupon.list" :key="index">
<view class="coupon-table-td">{{ item.coupon_name }}</view>
<view class="coupon-table-td">{{ item.money }}</view>
<view class="coupon-table-td">{{ item.validity_name }}</view>
<view class="coupon-table-td">
<view class="item-num">
<view class="num-dec" v-on:click="dec(item)">-</view>
<input class="table-input" type="text" v-model="item.num" />
<view class="num-inc" v-on:click="inc(item)">+</view>
</view>
</view>
</view>
<view class="empty" v-if="!sendCoupon.list.length">
<view class="iconfont iconwushuju"></view>
<view>暂无数据</view>
</view>
</scroll-view>
</view>
<view class="pop-bottom">
<button v-if="sendCoupon.list.length" class="primary-btn" @click="sendCouponFn">发放优惠券</button>
</view>
</view>
</uni-popup>
<!-- 办理会员 -->
<uni-popup ref="applyMemberPop">
<view class="pop-box applyMemberPop-box">
<view class="pop-header">
<view class="pop-header-text">办理会员</view>
<view class="pop-header-close" @click="popClose('applyMember')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<view class="common-scrollbar pop-content">
<view class="form-content">
<view class="form-item">
<view class="form-label">
<text class="required"></text>
会员等级
</view>
<view class="form-inline">
<select-lay :zindex="10" :value="applyMember.level_id" name="names" placeholder="请选择会员等级" :options="memberLevelList" @selectitem="selectMemberLevel"/>
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
会员卡号
</view>
<view class="form-inline">
<input class="form-input" type="text" placeholder="请输入会员卡号" v-model="applyMember.member_code" />
<view class="word-aux">会员卡号为会员唯一编号若不设置将会自动生成</view>
</view>
</view>
</view>
</view>
<view class="pop-bottom">
<button class="primary-btn" @click="saveApplyMember">确定</button>
</view>
</view>
</uni-popup>
<!-- 会员卡项弹出框 -->
<ns-member-card-popup v-if="isShowMemberCard" ref="memberCardPopup"/>
</view>
</template>
<script>
import dataTable from '@/components/uni-data-table/uni-data-table.vue';
import index from './index.js';
import UniPopup from "../uni-popup/uni-popup";
export default {
components: {
UniPopup,
dataTable
},
props:{
// 是否展示会员卡项
isShowMemberCard:{
type:Boolean,
default:false
}
},
mixins: [index],
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>
<style>
.member-info-pop >>> .pop-content, .member-info-pop >>> .uni-scroll-view{
overflow: inherit !important;
}
</style>

View File

@@ -0,0 +1,502 @@
import {
getMemberInfoById,
getMemberLevelList,
getCouponTypeList,
sendMemberCoupon,
editMember,
modifyMemberPoint,
modifyMemberBalance,
modifyMemberGrowth,
applyingMembershipCard
} from '@/api/member'
export default {
props: {
memberId: {
type: [String, Number],
default: 0
}
},
data() {
return {
pageSize: 8,
endTime: '',
memberInfo: null,
sex: [{
text: '未知',
value: 0
}, {
text: '男',
value: 1
}, {
text: '女',
value: 2
}],
pointData: {
num: 0,
desc: ''
},
growthData: {
num: 0,
desc: ''
},
balanceData: {
num: 0,
desc: ''
},
option: {},
sendCoupon: {
list: [],
page: 1
},
memberLevelList: [],
applyMember: {
level_id: '',
member_level_name: '',
member_code: ''
},
couponCols: [{
width: 15,
title: '优惠券名称',
align: 'left',
field: 'coupon_name'
}, {
width: 7,
title: '类型',
align: 'left',
templet: function (data) {
if (data.type == 'reward') return '满减';
if (data.type == 'discount') return '折扣';
}
}, {
width: 18,
title: '优惠金额',
align: 'left',
templet: function (data) {
if (data.type == 'reward') {
var html = `${data.at_least}元减${data.money}`;
return `<view title="${html}">${html}</view>`;
}
if (data.type == 'discount') {
var text = '满' + data.at_least + '元打' + data.discount + '折';
if (data.discount_limit) text += '(最多抵扣' + data.discount_limit + '元)';
return '<view title="' + text + '">' + text + '</view>';
}
}
}, {
width: 17,
title: '有效期',
align: 'center',
templet: data => {
if (data.end_time) return this.$util.timeFormat(data.end_time);
else return '长期有效';
}
}, {
width: 10,
title: '状态',
align: 'center',
return: data => {
if (data.state == 1) return '未使用';
if (data.state == 2) return '已使用';
if (data.state == 3) return '已过期';
}
},{
title: '适用场景',
field: 'use_channel_name',
width: 15,
align: 'left',
}, {
width: 18,
title: '领取时间',
align: 'right',
templet: data => {
return this.$util.timeFormat(data.fetch_time);
}
}],
pointCols: [{
width: 20,
title: '积分',
align: 'left',
field: 'account_data'
}, {
width: 25,
title: '发生方式',
align: 'left',
field: 'type_name'
}, {
width: 25,
title: '发生时间',
align: 'left',
templet: data => {
var html = this.$util.timeFormat(data.create_time);
return html;
}
}, {
width: 30,
title: '备注',
align: 'left',
field: 'remark'
}],
balanceCols: [{
width: 10,
title: '账户类型',
align: 'left',
field: 'account_type_name'
}, {
width: 15,
title: '余额',
align: 'left',
field: 'account_data'
}, {
width: 20,
title: '发生方式',
align: 'left',
field: 'type_name'
}, {
width: 25,
title: '发生时间',
align: 'left',
templet: data => {
var html = this.$util.timeFormat(data.create_time);
return html;
}
}, {
width: 30,
title: '备注',
align: 'left',
field: 'remark'
}],
growthCols: [{
width: 20,
title: '成长值',
align: 'left',
field: 'account_data'
}, {
width: 25,
title: '发生方式',
align: 'left',
field: 'type_name'
}, {
width: 25,
title: '发生时间',
align: 'left',
templet: data => {
var html = this.$util.timeFormat(data.create_time);
return html;
}
}, {
width: 30,
title: '备注',
align: 'left',
field: 'remark'
}],
};
},
created() {
this.getMemberInfo();
this.getMemberLevel();
let date = new Date();
var y = date.getFullYear();
var m = date.getMonth() + 1;
var d = date.getDate();
this.endTime = y + '-' + m + '-' + d;
},
watch: {
memberId: function () {
this.getMemberInfo();
}
},
methods: {
checkAdmin() {
if (this.userInfo && this.userInfo.is_admin == 0) {
// 检查当前账号是否有修改手机号的权限
var isAgree = false;
this.userInfo.user_group_list.forEach((item) => {
if (item.store_id == this.globalStoreInfo.store_id) {
if (item.menu_array.indexOf('member_edit') != -1) {
isAgree = true;
}
}
});
if (isAgree) {
return false;
}
}
return true;
},
getMemberInfo() {
getMemberInfoById(this.memberId).then(res => {
if (res.code >= 0) {
res.data.birthday = res.data.birthday > 0 ? this.$util.timeFormat(res.data.birthday, 'Y-m-d') : '';
this.memberInfo = res.data;
}
});
},
getMemberLevel() {
this.memberLevelList = [];
getMemberLevelList().then(res => {
if (res.code == 0 && res.data) {
for (let i in res.data) {
this.memberLevelList.push({
label: res.data[i]['level_name'],
value: res.data[i]['level_id'].toString(),
disabled: false
});
}
}
});
},
selectMemberLevel(index, item) {
if (index >= 0) {
this.applyMember.level_id = item.value;
this.applyMember.member_level_name = item.label;
this.memberInfo.member_level = item.value;
} else {
this.applyMember.level_id = '';
this.applyMember.member_level_name = '';
this.memberInfo.member_level = '';
}
},
// 客户操作
memberAction(type) {
switch (type) {
case 'memberInfo':
this.$refs.memberInfoPop.open('center');
break;
case 'point':
this.$refs.pointPop.open('center');
break;
case 'balance':
this.$store.commit('app/setGlobalMemberInfo', this.memberInfo);
this.$util.redirectTo('/pages/recharge/index');
break;
case 'sendCoupon':
this.getCouponList();
this.$refs.sendCouponPop.open('center');
break;
case 'growth':
this.$refs.growthPop.open('center');
break;
case 'couponList':
this.option = {
member_id: this.memberId
};
this.$refs.couponListPop.open('center');
break;
case 'cardList':
this.option = {
member_id: this.memberId,
status:1,
};
this.$refs.memberCardRecord.open('center');
break;
case 'pointList':
// 积分列表
this.option = {
member_id: this.memberId,
account_type: 'point'
};
this.$refs.pointListPop.open();
break;
case 'balanceList':
// 余额列表
this.option = {
member_id: this.memberId,
account_type: 'balance'
};
this.$refs.balanceListPop.open();
break;
case 'growthList':
// 成长值列表
this.option = {
member_id: this.memberId,
account_type: 'growth'
};
this.$refs.growthListPop.open();
break;
case 'applyMember':
this.$refs.applyMemberPop.open();
break;
}
},
popClose(type) {
this.$refs[type + 'Pop'].close();
},
//获取发放优惠券列表
getCouponList() {
let data = {
page: this.sendCoupon.page,
page_size: 7
};
getCouponTypeList(data).then(res => {
if (res.code >= 0) {
if (this.sendCoupon.page == 1) this.sendCoupon.list = [];
if (res.data.list && res.data.list.length) {
res.data.list.forEach((item, index) => {
if (item.validity_type == 0) item.validity_name = '失效日期:' + this.$util.timeFormat(item.end_time);
else if (item.validity_type == 1) item.validity_name = '领取后,' + item.fixed_term + '天有效';
else item.validity_name = '长期有效';
item.num = 0;
});
}
this.sendCoupon.list = this.sendCoupon.list.concat(res.data.list);
if (res.data.page_count >= this.sendCoupon.page) this.sendCoupon.page++;
}
});
},
// 发放数量
dec: function (item) {
if (item.num > 0) {
item.num = item.num - 1;
}
},
inc: function (item) {
item.num = item.num + 1;
},
// 发放优惠券
sendCouponFn() {
if (!this.sendCoupon.list || !this.sendCoupon.list.length) return false;
let data = {};
data.member_id = this.memberInfo.member_id;
data.coupon_data = '';
let couponDataArr = [];
this.sendCoupon.list.forEach((item, index) => {
if (item.num > 0) {
let obj = {};
obj.coupon_type_id = item.coupon_type_id;
obj.num = item.num;
couponDataArr.push(obj);
}
});
if (couponDataArr.length <= 0) return false;
data.coupon_data = JSON.stringify(couponDataArr);
sendMemberCoupon(data).then(res => {
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.sendCoupon.page = 1;
this.sendCoupon.list = [];
this.getMemberInfo();
this.$refs.sendCouponPop.close();
}
});
},
//修改客户信息
saveMemberInfo() {
let data = {
nickname: this.memberInfo.nickname,
sex: this.memberInfo.sex,
birthday: this.memberInfo.birthday,
member_id: this.memberInfo.member_id,
level_id: this.memberInfo.member_level
};
if (this.checkAdmin()) {
data.mobile = this.memberInfo.mobile;
}
editMember(data).then(res => {
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.getMemberInfo();
this.popClose('memberInfo');
}
});
},
// 调整积分
savePoint() {
if (parseInt(this.pointData.num) < 0 && parseInt(this.memberInfo.point) < parseInt(this.pointData.num * -1)) {
this.$util.showToast({
title: '调整数额与当前积分之和不能小于0'
});
return false;
}
modifyMemberPoint({
member_id: this.memberInfo.member_id,
adjust_num: this.pointData.num,
remark: this.pointData.desc
}).then(res => {
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.pointData.num = 0;
this.pointData.desc = '';
this.getMemberInfo();
this.popClose('point');
}
});
},
// 调整余额
saveBalance() {
modifyMemberBalance({
member_id: this.memberInfo.member_id,
adjust_num: this.balanceData.num,
remark: this.balanceData.desc
}).then(res => {
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.balanceData.num = 0;
this.balanceData.desc = '';
this.getMemberInfo();
this.popClose('balance');
}
});
},
// 调整成长值
saveGrowth() {
if (parseInt(this.growthData.num) < 0 && parseInt(this.memberInfo.growth) < parseInt(this.growthData.num * -1)) {
this.$util.showToast({
title: '调整数额与当前成长值之和不能小于0'
});
return false;
}
modifyMemberGrowth({
member_id: this.memberInfo.member_id,
adjust_num: this.growthData.num,
remark: this.growthData.desc
}).then(res => {
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.growthData.num = 0;
this.growthData.desc = '';
this.getMemberInfo();
this.popClose('growth');
}
});
},
// 办理会员卡
saveApplyMember() {
if (!this.applyMember.level_id) {
this.$util.showToast({
title: '请选择会员卡等级'
});
return false;
}
applyingMembershipCard({
member_id: this.memberInfo.member_id,
level_id: this.applyMember.level_id,
member_code: this.applyMember.member_code
}).then(res => {
this.$util.showToast({
title: res.message
});
if (res.code >= 0) {
this.$root.page = 1;
this.$root.search_text = 1;
this.$root.getMemberListFn();
this.popClose('applyMember');
}
});
},
headError(item) {
item.headimg = this.defaultImg.head;
}
}
}

View File

@@ -0,0 +1,453 @@
.member-detail-wrap {
width: 100%;
border-left: 0;
.member-head {
height: 0.66rem;
line-height: 0.66rem;
box-sizing: border-box;
border-bottom: 0.01rem solid #e6e6e6;
font-size: 0.14rem;
}
.member-content {
padding: 0.15rem;
width: 100%;
height: calc(100vh - 0.8rem);
box-sizing: border-box;
.content-block {
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
.item-img {
width: 0.7rem;
height: 0.7rem;
border-radius: 50%;
box-sizing: border-box;
image {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.item-content {
padding-left: 0.15rem;
width: calc(100% - 0.7rem);
box-sizing: border-box;
.item-title {
width: 100%;
font-size: 0.16rem;
align-items: center;
display: flex;
.item-label {
border: 0.01rem solid $primary-color;
color: $primary-color;
background-color: #fff;
border-radius: 0.02rem;
width: fit-content;
padding: 0.01rem 0.05rem;
margin-left: 0.15rem;
}
.item-title-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 50%;
font-size: 0.16rem;
}
}
.info-list {
margin-top: 0.15rem;
display: flex;
justify-content: space-between;
.info-item {
font-size: 0.14rem;
margin-right: 0.2rem;
}
}
}
}
.content-block.account {
border: 0.01rem solid #e6e6e6;
background-color: #ffffff;
padding: 0.25rem 0;
display: flex;
justify-content: space-around;
margin-top: 0.2rem;
border-radius: 0.03rem;
align-items: baseline;
.content-data-item {
.data-item-title {}
.data-item-value {
font-size: 0.26rem;
margin-top: 0.1rem;
}
.data-item-action {
margin-top: 0.1rem;
color: $primary-color;
cursor: pointer;
float: left;
&:nth-child(2n) {
margin-left: 0.1rem;
}
}
}
}
.content-block.assets {
display: flex;
justify-content: space-around;
margin-top: 0.2rem;
.content-data-left {
background-color: #ffffff;
padding: 0.25rem 0;
border-radius: 0.03rem;
width: calc(50% - 0.075rem);
margin-right: 0.15rem;
display: flex;
justify-content: space-around;
height: 1rem;
.content-data-item {
.data-item-title {}
.data-item-value {
font-size: 0.26rem;
margin-top: 0.1rem;
}
.data-item-action {
margin-top: 0.15rem;
color: $primary-color;
cursor: pointer;
}
}
}
}
.content-block.action {
display: flex;
justify-content: flex-start;
margin-top: 0.2rem;
.content-data-item {
border: 0.01rem solid #e6e6e6;
width: calc(100% / 6);
background-color: #ffffff;
display: flex;
padding: 0.15rem 0;
border-radius: 0.03rem;
align-items: center;
text-align: center;
flex-direction: column;
margin-right: 0.15rem;
cursor: pointer;
.data-item-icon {
width: 0.55rem;
height: 0.55rem;
image {
width: 100%;
height: 100%;
}
}
.data-item-value {
margin-top: 0.1rem;
}
&:last-child {
margin-right: 0;
}
}
}
}
}
// pop弹框
.pop-box {
background: #ffffff;
width: 8rem;
height: 7rem;
.pop-header {
padding: 0 0.15rem 0 0.2rem;
height: 0.5rem;
line-height: 0.5rem;
border-bottom: 0.01rem solid #f0f0f0;
font-size: 0.14rem;
color: #333;
overflow: hidden;
border-radius: 0.02rem 0.2rem 0 0;
box-sizing: border-box;
display: flex;
justify-content: space-between;
.pop-header-close {
cursor: pointer;
text {
font-size: 0.18rem;
}
}
}
.pop-content {
height: calc(100% - 1.05rem);
overflow-y: auto;
padding: 0.1rem 0.2rem;
box-sizing: border-box;
}
.pop-bottom {
width: 100%;
box-sizing: border-box;
padding: 0.1rem 0.2rem;
button {
width: 100%;
line-height: 0.35rem;
height: 0.35rem;
}
}
}
//表单
.form-content {
display: flex;
flex-direction: column;
align-items: center;
.form-item {
margin-bottom: 0.1rem;
display: flex;
&:last-of-type {
margin-bottom: 0;
}
.form-label {
width: 1.2rem;
text-align: right;
padding-right: 0.1rem;
box-sizing: border-box;
height: 0.32rem;
line-height: 0.32rem;
.required {
color: red;
margin-right: 0.03rem;
}
}
.form-inline {
width: 2.5rem;
line-height: 0.32rem;
box-sizing: border-box;
.form-input {
border-width: 0.01rem;
border-style: solid;
background-color: #fff;
color: rgba(0, 0, 0, 0.85);
border-radius: 0.02rem;
padding-left: 0.1rem;
height: 0.32rem;
line-height: 0.32rem;
font-size: 0.14rem;
border-color: #e6e6e6;
}
.form-textarea {
border-width: 0.01rem;
border-style: solid;
background-color: #fff;
color: rgba(0, 0, 0, 0.85);
border-radius: 0.02rem;
padding-left: 0.1rem;
line-height: 0.32rem;
font-size: 0.14rem;
border-color: #e6e6e6;
width: 95%;
}
.word-aux {
color: #999;
font-size: 0.12rem;
line-height: 1.5;
margin-top: 0.05rem;
}
}
}
}
.memberInfo-box {
width: 5.2rem;
height: 4.31rem;
}
.pointPop-box {
width: 4.2rem;
height: 3.94rem;
}
.balancePop-box {
width: 4.2rem;
height: 4.2rem;
}
.coupon-list-pop-box {
width: 10rem;
height: 5.7rem;
.pop-content {
height: calc(100% - 0.5rem);
}
/deep/ .tpage {
position: absolute;
right: 0;
bottom: 0;
}
}
.applyMemberPop-box {
width: 4.2rem;
height: 3.38rem;
.pop-content {
overflow: initial;
}
}
.sendCoupon-box {
width: 9rem;
height: 5.06rem;
.sendCoupon-content {
padding: 0.1rem 0.2rem;
.coupon-table-head {
display: flex;
background: #f7f8fa;
}
.coupon-table-body {
height: 3.2rem;
.coupon-table-tr {
display: flex;
border-bottom: 0.01rem solid #e6e6e6;
}
.table-input {
height: 0.3rem;
line-height: 0.3rem;
border: 0.01rem solid #e6e6e6;
padding: 0 0.1rem;
text-align: center;
}
.item-num {
display: flex;
align-items: center;
margin-left: 0.1rem;
.num-dec {
width: 0.6rem;
height: 0.25rem;
background: #e6e6e6;
border: 0.01rem solid #e6e6e6;
border-radius: 30%;
text-align: center;
line-height: 0.23rem;
font-size: 0.25rem;
margin-right: 0.1rem;
cursor: pointer;
transition: 0.3s;
}
.num-inc {
width: 0.6rem;
height: 0.25rem;
background: $primary-color;
border: 0.01rem solid #e6e6e6;
border-radius: 30%;
text-align: center;
line-height: 0.23rem;
font-size: 0.25rem;
margin-left: 0.1rem;
cursor: pointer;
transition: 0.3s;
color: #fff;
}
}
.coupon-table-td:nth-child(4) {
padding: 0 0.05rem;
}
}
.coupon-table-td,
.coupon-table-th {
padding: 0 0.1rem;
display: flex;
align-items: center;
height: 0.5rem;
&:nth-child(1) {
flex-basis: 30%;
}
&:nth-child(2) {
flex-basis: 20%;
}
&:nth-child(3) {
flex-basis: 30%;
}
&:nth-child(4) {
justify-content: flex-end;
flex-basis: 20%;
text-align: right;
}
}
}
.pop-bottom {
margin-top: 0.12rem;
}
.empty {
display: flex;
align-items: center;
justify-content: center;
height: 0.5rem;
border-bottom: 0.01rem solid #e6e6e6;
color: #909399;
.iconfont {
font-size: 0.25rem;
margin: 0.05rem;
}
}
}

View File

@@ -0,0 +1,500 @@
<template>
<view class="member-detail-wrap">
<view class="member-head flex items-center justify-between">
<text>会员详情</text>
<text class="iconfont iconguanbi1 cursor-pointer" @click="$emit('close')"></text>
</view>
<view class="member-content">
<view class="content-block">
<view class="item-img">
<image mode="aspectFill" v-if="memberInfo && memberInfo.headimg" :src="$util.img(memberInfo.headimg)" @error="headError(memberInfo)"/>
<image mode="aspectFill" v-else :src="$util.img(defaultImg.head)"/>
</view>
<view class="item-content">
<view class="item-title">
<view class="item-title-text">{{ memberInfo && memberInfo.nickname ? memberInfo.nickname : '' }}</view>
<view class="item-label" v-if="memberInfo && memberInfo.member_level && memberInfo.member_level_name">{{ memberInfo.member_level_name }}</view>
</view>
<view class="info-list">
<view class="info-item">手机{{ memberInfo && memberInfo.mobile ? memberInfo.mobile : '' }}</view>
<view class="info-item" v-if="memberInfo && memberInfo.sex == 0">性别未知</view>
<view class="info-item" v-if="memberInfo && memberInfo.sex == 1">性别</view>
<view class="info-item" v-if="memberInfo && memberInfo.sex == 2">性别</view>
<view class="info-item">生日{{ memberInfo && memberInfo.birthday ? memberInfo.birthday : '' }}</view>
<view class="info-item" v-if="memberInfo && memberInfo.member_time">成为会员{{ $util.timeFormat(memberInfo.member_time) }}</view>
</view>
</view>
</view>
<view class="content-block account">
<view class="content-data-item">
<view class="data-item-title">积分</view>
<view class="data-item-value">{{ memberInfo && memberInfo.point ? parseInt(memberInfo.point) : '0' }}</view>
<view class="data-item-action" @click="memberAction('pointList')">查看</view>
</view>
<view class="content-data-item">
<view class="data-item-title">储值余额()</view>
<view class="data-item-value">{{ memberInfo && memberInfo.balance ? memberInfo.balance : '0.00' }}</view>
<view class="data-item-action" @click="memberAction('balanceList')">查看</view>
</view>
<view class="content-data-item">
<view class="data-item-title">现金余额()</view>
<view class="data-item-value">{{ memberInfo && memberInfo.balance_money ? memberInfo.balance_money : '0.00' }}</view>
</view>
<view class="content-data-item">
<view class="data-item-title">成长值</view>
<view class="data-item-value">{{ memberInfo && memberInfo.growth ? memberInfo.growth : '0' }}</view>
<view class="data-item-action" @click="memberAction('growthList')">查看</view>
</view>
<view class="content-data-item">
<view class="data-item-title">优惠券()</view>
<view class="data-item-value">{{ memberInfo && memberInfo.coupon_num ? memberInfo.coupon_num : '0' }}</view>
<view class="data-item-action" @click="memberAction('couponList')">查看</view>
</view>
<view class="content-data-item">
<view class="data-item-title">卡包</view>
<view class="data-item-value">{{ memberInfo && memberInfo.card_num ? memberInfo.card_num : '0' }}
</view>
<view class="data-item-action" @click="memberAction('cardList')">查看</view>
</view>
</view>
<view class="content-block action">
<view class="content-data-item" @click="memberAction('memberInfo')">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-info.png" />
</view>
<view class="data-item-value">会员信息</view>
</view>
<view class="content-data-item" @click="memberAction('point')">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-point.png" />
</view>
<view class="data-item-value">积分调整</view>
</view>
<view class="content-data-item" @click="memberAction('balance')">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-balance.png" />
</view>
<view class="data-item-value">余额充值</view>
</view>
<view class="content-data-item" @click="memberAction('sendCoupon')">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-coupon.png" />
</view>
<view class="data-item-value">送优惠券</view>
</view>
<view class="content-data-item" @click="memberAction('growth')">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-growth.png" />
</view>
<view class="data-item-value">成长值调整</view>
</view>
<view class="content-data-item" @click="memberAction('applyMember')" v-if="memberInfo && !memberInfo.is_member">
<view class="data-item-icon">
<image mode="aspectFit" src="@/static/member/icon-member-apply.png" />
</view>
<view class="data-item-value">办理会员</view>
</view>
</view>
</view>
<!-- 会员详情 -->
<uni-popup ref="memberInfoPop">
<view class="pop-box memberInfo-box">
<view class="pop-header">
<view class="pop-header-text">会员详情</view>
<view class="pop-header-close" @click="popClose('memberInfo')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<view class="form-content" v-if="memberInfo">
<view class="form-item">
<view class="form-label">
<text class="required"></text>
昵称
</view>
<view class="form-inline">
<input class="form-input" placeholder="请输入会员昵称" v-model="memberInfo.nickname" />
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
手机号
</view>
<view class="form-inline">
<input class="form-input" placeholder="请输入手机号" v-model="memberInfo.mobile" maxlength="11" />
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
会员等级
</view>
<view class="form-inline">
<select-lay :zindex="10" :value="memberInfo.member_level" name="names" placeholder="请选择会员等级" :options="memberLevelList" @selectitem="selectMemberLevel"/>
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
性别
</view>
<view class="form-inline">
<uni-data-checkbox v-model="memberInfo.sex" :localdata="sex"></uni-data-checkbox>
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
生日
</view>
<view class="form-inline">
<uni-datetime-picker :end="endTime" v-model="memberInfo.birthday" type="date" :clearIcon="false" />
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
注册时间
</view>
<view class="form-inline">
{{ memberInfo && memberInfo.reg_time ? $util.timeFormat(memberInfo.reg_time) : '--' }}
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
最后访问时间
</view>
<view class="form-inline">
{{ memberInfo && memberInfo.last_login_time ? $util.timeFormat(memberInfo.last_login_time) : '--' }}
</view>
</view>
</view>
</scroll-view>
<view class="pop-bottom">
<button class="primary-btn" @click="saveMemberInfo">确定</button>
</view>
</view>
</uni-popup>
<!-- 积分调整 -->
<uni-popup ref="pointPop">
<view class="pop-box pointPop-box">
<view class="pop-header">
<view class="pop-header-text">调整积分</view>
<view class="pop-header-close" @click="popClose('point')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<view class="form-content">
<view class="form-item">
<view class="form-label">
<text class="required"></text>
当前积分
</view>
<view class="form-inline">{{ memberInfo && memberInfo.point ? memberInfo.point : '0' }}</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
调整数额
</view>
<view class="form-inline">
<input class="form-input" type="number" placeholder="请输入调整数额" v-model="pointData.num" />
<view class="word-aux">调整数额与当前积分数相加不能小于0</view>
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
备注
</view>
<view class="form-inline">
<textarea class="form-textarea" v-model="pointData.desc"/>
</view>
</view>
</view>
</scroll-view>
<view class="pop-bottom">
<button class="primary-btn" @click="savePoint">确定</button>
</view>
</view>
</uni-popup>
<!-- 发放优惠券 -->
<uni-popup ref="sendCouponPop">
<view class="pop-box sendCoupon-box">
<view class="pop-header">
<view class="pop-header-text">送优惠券</view>
<view class="pop-header-close" @click="popClose('sendCoupon')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<view class="common-scrollbar sendCoupon-content">
<view class="coupon-table-head">
<view class="coupon-table-th">优惠券名称</view>
<view class="coupon-table-th">金额</view>
<view class="coupon-table-th">有效期</view>
<view class="coupon-table-th">发放数量</view>
</view>
<scroll-view class="coupon-table-body" @scrolltolower="getCouponList()" scroll-y="true">
<view class="coupon-table-tr" v-for="(item, index) in sendCoupon.list" :key="index">
<view class="coupon-table-td">{{ item.coupon_name }}</view>
<view class="coupon-table-td">{{ item.money }}</view>
<view class="coupon-table-td">{{ item.validity_name }}</view>
<view class="coupon-table-td">
<view class="item-num">
<view class="num-dec" v-on:click="dec(item)">-</view>
<input class="table-input" type="text" v-model="item.num" />
<view class="num-inc" v-on:click="inc(item)">+</view>
</view>
</view>
</view>
<view class="empty" v-if="!sendCoupon.list.length">
<view class="iconfont iconwushuju"></view>
<view>暂无数据</view>
</view>
</scroll-view>
</view>
<view class="pop-bottom">
<button v-if="sendCoupon.list.length" class="primary-btn" @click="sendCouponFn">发放优惠券</button>
</view>
</view>
</uni-popup>
<!-- 余额调整 -->
<uni-popup ref="balancePop">
<view class="pop-box pointPop-box">
<view class="pop-header">
<view class="pop-header-text">调整余额</view>
<view class="pop-header-close" @click="popClose('balance')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<view class="form-content">
<view class="form-item">
<view class="form-label">
<text class="required"></text>
当前余额
</view>
<view class="form-inline">
{{ memberInfo && memberInfo.balance ? memberInfo.balance : '0.00' }}
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
调整数额
</view>
<view class="form-inline">
<input class="form-input" type="number" placeholder="请输入调整数额" v-model="balanceData.num" />
<view class="word-aux">调整数额与当前储值余额相加不能小于0</view>
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
备注
</view>
<view class="form-inline">
<textarea class="form-textarea" v-model="balanceData.desc"></textarea>
</view>
</view>
</view>
</scroll-view>
<view class="pop-bottom">
<button class="primary-btn" @click="saveBalance">确定</button>
</view>
</view>
</uni-popup>
<!-- 成长值调整 -->
<uni-popup ref="growthPop">
<view class="pop-box pointPop-box">
<view class="pop-header">
<view class="pop-header-text">调整成长值</view>
<view class="pop-header-close" @click="popClose('growth')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<view class="form-content">
<view class="form-item">
<view class="form-label">
<text class="required"></text>
当前成长值
</view>
<view class="form-inline">{{ memberInfo && memberInfo.growth ? memberInfo.growth : '0' }}</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
调整数额
</view>
<view class="form-inline">
<input class="form-input" type="number" placeholder="请输入调整数额" v-model="growthData.num" />
<view class="word-aux">调整数额与当前成长值相加不能小于0</view>
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
备注
</view>
<view class="form-inline">
<textarea class="form-textarea" v-model="growthData.desc"></textarea>
</view>
</view>
</view>
</scroll-view>
<view class="pop-bottom">
<button class="primary-btn" @click="saveGrowth">确定</button>
</view>
</view>
</uni-popup>
<!-- 办理会员 -->
<uni-popup ref="applyMemberPop">
<view class="pop-box applyMemberPop-box">
<view class="pop-header">
<view class="pop-header-text">办理会员</view>
<view class="pop-header-close" @click="popClose('applyMember')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<view class="common-scrollbar pop-content">
<view class="form-content">
<view class="form-item">
<view class="form-label">
<text class="required"></text>
会员等级
</view>
<view class="form-inline">
<select-lay :zindex="10" :value="applyMember.level_id" name="names" placeholder="请选择会员等级" :options="memberLevelList" @selectitem="selectMemberLevel"/>
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
会员卡号
</view>
<view class="form-inline">
<input class="form-input" type="text" placeholder="请输入会员卡号" v-model="applyMember.member_code" />
<view class="word-aux">会员卡号为会员唯一编号若不设置将会自动生成</view>
</view>
</view>
</view>
</view>
<view class="pop-bottom">
<button class="primary-btn" @click="saveApplyMember">确定</button>
</view>
</view>
</uni-popup>
<!-- 优惠券列表 -->
<uni-popup ref="couponListPop">
<view class="pop-box coupon-list-pop-box">
<view class="pop-header">
<view class="pop-header-text">优惠券</view>
<view class="pop-header-close" @click="popClose('couponList')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<dataTable url="/cashier/storeapi/member/coupon" :cols="couponCols" ref="table" :option="option" :pagesize="pageSize"></dataTable>
</scroll-view>
</view>
</uni-popup>
<!-- 积分列表 -->
<uni-popup ref="pointListPop">
<view class="pop-box coupon-list-pop-box">
<view class="pop-header">
<view class="pop-header-text">积分</view>
<view class="pop-header-close" @click="popClose('pointList')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<dataTable url="/cashier/storeapi/member/memberaccountlist" :cols="pointCols" ref="table" :option="option" :pagesize="pageSize"></dataTable>
</scroll-view>
</view>
</uni-popup>
<!-- 余额列表 -->
<uni-popup ref="balanceListPop">
<view class="pop-box coupon-list-pop-box">
<view class="pop-header">
<view class="pop-header-text">余额</view>
<view class="pop-header-close" @click="popClose('balanceList')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<dataTable url="/cashier/storeapi/member/memberaccountlist" :cols="balanceCols" ref="table" :option="option" :pagesize="pageSize"></dataTable>
</scroll-view>
</view>
</uni-popup>
<!-- 成长值列表 -->
<uni-popup ref="growthListPop">
<view class="pop-box coupon-list-pop-box">
<view class="pop-header">
<view class="pop-header-text">成长值</view>
<view class="pop-header-close" @click="popClose('growthList')">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" class="common-scrollbar pop-content">
<dataTable url="/cashier/storeapi/member/memberaccountlist" :cols="growthCols" ref="table" :option="option" :pagesize="pageSize"></dataTable>
</scroll-view>
</view>
</uni-popup>
<!-- 卡项 -->
<ns-member-card-record ref="memberCardRecord" :option="option"/>
</view>
</template>
<script>
import dataTable from '@/components/uni-data-table/uni-data-table.vue';
import nsMemberCardRecord from '@/components/ns-member-card-record/ns-member-card-record.vue';
import index from './index.js';
export default {
components: {
dataTable,
nsMemberCardRecord
},
mixins: [index]
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@@ -0,0 +1,106 @@
<template>
<view class="journal">
<view class="item" v-for="(item, index) in list" :key="index">
<view class="time">
<view>{{ $util.timeFormat(item.action_time).split(' ')[0] }}</view>
<view>{{ $util.timeFormat(item.action_time).split(' ')[1] }}</view>
</view>
<view class="unit">
<view class="top">
<view class="core"></view>
<view class="unit-separate"></view>
</view>
</view>
<view class="message">{{ item.action }}</view>
</view>
</view>
</template>
<script>
export default {
props: {
list: {
type: Array,
default: function() {
return [];
}
}
},
data() {
return {};
},
mounted() {},
methods: {}
};
</script>
<style scoped lang="scss">
.journal {
padding-left: 0.1rem;
box-sizing: border-box;
.item {
width: 100%;
height: 0.7rem;
display: flex;
.time {
margin-right: 0.1rem;
min-width: 1rem;
view:nth-child(1) {
font-size: 0.16rem;
margin-bottom: 0.1rem;
text-align: right;
}
view:nth-child(2) {
font-size: 0.14rem;
color: #999999;
text-align: right;
}
}
}
.unit {
width: 0.18rem;
height: 100%;
margin-right: 0.1rem;
.top {
width: 0.18rem;
height: 0.18rem;
border-radius: 50%;
background: $primary-color;
position: relative;
.core {
background: #ffffff;
width: 0.08rem;
height: 0.08rem;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 999;
}
.unit-separate {
position: absolute;
width: 0.01rem;
height: 0.7rem;
top: 0;
left: 50%;
transform: translateX(-50%);
background: $primary-color;
z-index: 555;
}
}
}
.message {
font-size: 0.14rem;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,786 @@
.container {
width: 100%;
height: 100%;
& > view {
width: 100%;
height: 100%;
}
}
.payment-wrap {
.header {
height: 0.66rem;
display: flex;
align-items: center;
border-bottom: 0.01rem solid #e6e6e6;
}
.body {
flex: 1;
height: 0;
padding: 0.15rem 0;
box-sizing: border-box;
display: flex;
}
.info-wrap {
flex: 1;
width: 0;
margin-right: 0.15rem;
display: flex;
flex-direction: column;
.info {
flex: 1;
height: 0;
background-color: #f7f8fa;
padding-bottom: 0.15rem;
box-sizing: border-box;
/deep/ .uni-scroll-view-content {
margin: 0 0.15rem;
width: calc(100% - 0.3rem);
box-sizing: border-box;
}
.payment-money {
text-align: right;
font-size: 0.2rem;
border-bottom: 0.01rem solid #e6e6e6;
line-height: 0.6rem;
}
.title {
line-height: 0.6rem;
font-size: 0.16rem;
}
.uni-flex {
flex-wrap: wrap;
}
.type-item {
padding: 0.2rem 0.1rem;
background: #fff;
border: 0.01rem solid #e6e6e6;
display: flex;
align-items: center;
font-size: 0.16rem;
margin: 0 0.1rem 0.1rem 0;
width: calc((100% - 0.86rem) / 3);
line-height: 1;
cursor: pointer;
position: relative;
border-radius: 0.02rem;
&.account {
width: calc((100% - 0.86rem) / 2);
}
.name {
flex: 1;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:nth-child(3n + 3) {
margin-right: 0;
}
.iconfont {
color: $primary-color;
font-size: 0.3rem;
margin-right: 0.1rem;
}
.text {
color: #fe2278;
margin-left: 0.05rem;
}
.iconxuanzhong {
position: absolute;
display: none;
}
&.active {
border-color: $primary-color;
.iconxuanzhong {
display: block;
right: -0.11rem;
bottom: -0.01rem;
}
}
&.disabled {
background: #f5f5f5;
cursor: not-allowed;
}
}
.pay-type {
.type-item {
padding: 0.15rem 0.1rem;
}
.pay-icon {
color: #fff;
background: #f0f0f0;
width: 0.3rem;
height: 0.3rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.16rem;
border-radius: 0.05rem;
}
}
}
.button-wrap {
padding-top: 0.15rem;
display: flex;
justify-content: flex-end;
align-items: center;
.scancode {
color: $primary-color;
}
button {
margin: 0 0 0 0.1rem;
min-width: 1rem;
}
.print-ticket {
flex: 1;
width: 0;
display: flex;
align-items: center;
}
}
}
.bill-wrap {
width: 3rem;
border: 0.01rem solid #e6e6e6;
.title {
text-align: center;
font-size: 0.2rem;
border-bottom: 0.01rem solid #e6e6e6;
line-height: 0.6rem;
}
.body {
padding: 0;
margin: 0 0.15rem;
display: block;
height: auto;
.block-title {
position: relative;
text-align: center;
width: 100%;
height: 0.35rem;
margin-top: 0.2rem;
text {
padding: 0 0.2rem;
background: #fff;
position: absolute;
left: 50%;
top: 50%;
z-index: 1;
transform: translate(-50%, -50%);
font-size: 0.16rem;
}
&::before {
content: '';
position: absolute;
width: 100%;
top: 50%;
left: 0;
border-top: 0.01rem dashed #e6e6e6;
}
}
.bill-info {
display: flex;
justify-content: space-between;
line-height: 1;
align-items: center;
margin-top: 0.2rem;
.text {
color: #fe2278;
}
}
}
}
.remark-info {
padding: 0.1rem;
background-color: var(--primary-color-light-9);
color: $primary-color;
margin-top: 0.1rem;
font-size: 0.12rem;
}
}
.pay-result {
.body {
flex: 1;
height: 0;
text-align: center;
&.status {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.iconfont {
font-size: 1rem;
color: $primary-color;
}
.msg {
margin-top: 0.1rem;
font-size: 0.16rem;
color: $primary-color;
}
}
}
.footer {
height: 0.66rem;
display: flex;
align-items: center;
border-top: 0.01rem solid #e6e6e6;
justify-content: center;
button {
margin: 0 0 0 0.15rem;
width: auto;
min-width: 1.6rem;
height: 0.45rem;
line-height: 0.45rem;
}
}
}
.money-wrap {
background: #fff;
border-radius: 0.05rem;
.head {
height: 0.6rem;
line-height: 0.6rem;
text-align: center;
font-weight: bold;
position: relative;
text {
font-size: 0.16rem;
}
.iconguanbi1 {
position: absolute;
right: 0.15rem;
font-size: 0.22rem;
cursor: pointer;
}
}
.content-wrap {
display: flex;
border: 0.01rem solid #e6e6e6;
height: 0.6rem;
align-items: center;
margin: 0 0.2rem;
padding: 0 0.15rem;
.unit {
font-size: 0.25rem;
}
.money {
margin-left: 0.05rem;
font-size: 0.2rem;
}
}
.keyboard-wrap {
width: 4rem;
padding: 0 0.2rem 0.3rem 0.2rem;
margin-top: 0.1rem;
}
}
.coupon-wrap {
background: #fff;
width: 6rem;
border-radius: 0.05rem;
.head {
height: 0.6rem;
line-height: 0.6rem;
text-align: center;
font-weight: bold;
position: relative;
text {
font-size: 0.16rem;
}
.iconguanbi1 {
position: absolute;
right: 0.15rem;
font-size: 0.22rem;
cursor: pointer;
}
}
.body {
height: 3rem;
}
.list {
display: flex;
padding: 0.1rem 0.15rem;
flex-wrap: wrap;
.item {
margin: 0 0.1rem 0.1rem 0;
padding: 0.1rem 0;
border: 0.01rem solid #e6e6e6;
width: calc((100% - 0.14rem) / 2);
cursor: pointer;
display: flex;
position: relative;
.iconxuanzhong {
position: absolute;
display: none;
right: -0.01rem;
bottom: -0.01rem;
font-size: 0.3rem;
}
&.active {
border-color: $primary-color;
.iconxuanzhong {
display: block;
color: $primary-color;
}
}
&:nth-child(2n + 2) {
margin-right: 0;
}
.money {
display: flex;
align-items: center;
justify-content: center;
min-height: 0.6rem;
min-width: 1rem;
font-size: 0.2rem;
line-height: 1;
.unit {
font-size: 0.16rem;
margin-top: 0.05rem;
font-weight: bold;
}
}
.info {
padding: 0 0.1rem;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.title {
font-weight: bold;
}
.time,
.limit {
font-size: 0.12rem;
color: #999;
line-height: 1;
margin-top: 0.05rem;
}
}
}
}
}
.safe-verify-popup {
width: 4.4rem;
height: 3.1rem;
background-color: #fff;
border-radius: 0.1rem;
.header {
height: 0.6rem;
line-height: 0.6rem;
text-align: center;
position: relative;
.type-wrap {
display: flex;
.item {
margin-left: 0.15rem;
font-size: 0.16rem;
cursor: pointer;
&.active {
font-size: 0.18rem;
color: $primary-color;
font-weight: bold;
}
}
}
.iconguanbi1 {
position: absolute;
right: 0.15rem;
top: 0;
font-size: 0.22rem;
cursor: pointer;
font-weight: bold;
}
}
.content {
padding: 0 0.3rem;
margin-top: 0.2rem;
}
.member-code-hint{
margin-top: .3rem;
font-size: .16rem;
}
.tips {
color: #999;
}
.mobile {
font-size: 0.25rem;
font-weight: bold;
margin-top: 0.05rem;
}
.sms-code {
display: flex;
align-items: center;
margin-top: 0.15rem;
border-bottom: 0.01rem solid #eee;
padding: 0.15rem 0;
view {
position: relative;
display: flex;
align-items: center;
flex: 1;
input {
flex: 1;
margin: 0 0.1rem 0 0;
padding: 0;
border-bottom: none;
font-size: 0.14rem;
}
text {
position: absolute;
right: 0.1rem;
font-size: 0.2rem;
color: #999;
cursor: pointer;
}
}
.send-tip {
color: $primary-color;
font-size: 0.13rem;
cursor: pointer;
&.disabled {
color: #999;
cursor: not-allowed;
}
}
}
.placeholder {
font-size: 0.14rem;
}
.primary-btn {
margin-top: 0.3rem;
line-height: 0.4rem;
}
.scancode-wrap {
text-align: center;
.input-wrap {
display: flex;
view {
position: relative;
display: flex;
align-items: center;
flex: 1;
input {
width: 0;
flex: 1;
height: 0.5rem;
border: 0.01rem solid #cccccc;
text-align: center;
padding: 0 0.1rem;
box-sizing: border-box;
transition: all 0.3s;
&.focus {
border-color: $primary-color;
box-shadow: 0 0 0.02rem 0.02rem var(--primary-color-light-7);
}
}
text {
position: absolute;
right: 0.1rem;
font-size: 0.2rem;
color: #999;
cursor: pointer;
}
}
.primary-btn {
margin: 0 0 0 0.1rem;
line-height: 0.5rem;
width: 1rem;
padding: 0;
}
}
image {
width: 3rem;
padding: 0.2rem 0.4rem;
box-sizing: border-box;
}
}
}
.third-popup {
width: 4rem;
height: 5rem;
background-color: #fff;
border-radius: 0.1rem;
display: flex;
flex-direction: column;
.head {
height: 0.8rem;
line-height: 0.8rem;
text-align: center;
position: relative;
text {
font-size: 0.25rem;
}
.iconguanbi1 {
position: absolute;
right: 0.15rem;
font-size: 0.22rem;
cursor: pointer;
font-weight: bold;
}
}
.money {
text-align: center;
font-size: 0.18rem;
color: $primary-color;
}
.scan-code-type {
display: flex;
width: 100%;
margin-top: 0.2rem;
background-color: #f5f5f5;
.type-item {
flex: 1;
text-align: center;
line-height: 0.5rem;
font-size: 0.16rem;
cursor: pointer;
border-bottom: 0.03rem solid #f5f5f5;
position: relative;
&.active {
border-bottom: 0.03rem solid $primary-color;
}
&:last-child::after {
content: ' ';
position: absolute;
left: 0;
top: 20%;
width: 0.01rem;
height: 60%;
background: #ddd;
}
}
}
.content-wrap {
flex: 1;
height: 0;
display: flex;
align-items: center;
justify-content: center;
.qrcode-wrap {
display: flex;
justify-content: center;
.empty {
padding: 1rem 0;
text-align: center;
}
.qrcode-item {
height: 1.3rem;
width: 1.3rem;
padding: 0.1rem;
box-shadow: 0 0.02rem 0.1rem 0 rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
position: relative;
.qrcode {
width: 100%;
}
.logo {
width: 0.25rem;
position: absolute !important;
z-index: 5;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&:nth-child(2) {
margin-left: 0.2rem;
}
}
}
.scancode-wrap {
view {
position: relative;
display: flex;
align-items: center;
input {
width: 3.3rem;
height: 0.5rem;
border: 0.01rem solid #cccccc;
text-align: center;
padding: 0 0.1rem;
box-sizing: border-box;
transition: all 0.3s;
&.focus {
border-color: $primary-color;
box-shadow: 0 0 0.02rem 0.02rem var(--primary-color-light-7);
}
}
text {
position: absolute;
right: 0.1rem;
font-size: 0.2rem;
color: #999;
cursor: pointer;
}
}
image {
width: 3.3rem;
padding: 0.2rem 0.4rem;
box-sizing: border-box;
}
}
}
}
.remark-wrap {
width: 6rem;
background-color: #fff;
border-radius: 0.04rem;
box-shadow: 0 0.01rem 0.12rem 0 rgba(0, 0, 0, 0.1);
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0.15rem;
height: 0.45rem;
line-height: 0.45rem;
border-bottom: 0.01rem solid #e8eaec;
.iconfont {
font-size: $uni-font-size-lg;
}
}
.body {
padding: 0.15rem 0.15rem 0.1rem;
textarea {
border: 0.01rem solid #e6e6e6;
width: 100%;
padding: 0.1rem;
box-sizing: border-box;
font-size: 0.14rem;
}
.placeholder-class {
font-size: 0.14rem;
}
}
.footer {
height: 0.5rem;
padding-bottom: 0.05rem;
display: flex;
align-items: center;
justify-content: center;
button {
width: 95%;
}
}
}

View File

@@ -0,0 +1,366 @@
<template>
<view class="container" v-if="payInfo">
<view class="uni-flex uni-column payment-wrap" v-show="payStatus == 'pay'">
<view class="header">结算</view>
<view class="body">
<view class="info-wrap">
<scroll-view scroll-y="true" class="info">
<view class="payment-money">费用总额{{ payInfo.original_money | moneyFormat }}</view>
<block v-if="promotionShow">
<view class="title">营销优惠</view>
<view class="uni-flex">
<view class="type-item" :class="{ disabled: payInfo.offset.coupon_array.member_coupon_list.length == 0, active: discount.coupon_id }" @click="selectCoupon" v-if="payInfo.offset.coupon_array">
<view class="iconfont iconyouhuiquan"></view>
<view class="name" v-show="!discount.coupon_id">
优惠券
<text class="text" v-if="payInfo.offset.coupon_array.member_coupon_list.length">
{{ payInfo.offset.coupon_array.member_coupon_list.length }}张可用
</text>
</view>
<view class="name" v-show="discount.coupon_id">
优惠券抵扣
<text class="text">{{ payInfo.coupon_money }}</text>
</view>
<view class="iconfont iconxuanzhong"></view>
</view>
<view class="type-item" :class="{ active: discount.reduction }" @click="reduction" v-if="payInfo.collectmoney_config.reduction == 1">
<view class="iconfont iconjianmianjine"></view>
<view class="name" v-if="discount.reduction" @click.stop="openMoneyPopup({ title: '减免金额', money: $util.moneyFormat(discount.reduction), type: 'reduction' })">
减免
<text class="text">{{ discount.reduction }}</text>
</view>
<view v-else class="name">减免金额</view>
<view class="iconfont iconxuanzhong"></view>
</view>
</view>
</block>
<block v-if="payInfo.offset.point_array || payInfo.offset.balance">
<view class="title">账户余额</view>
<view class="uni-flex">
<view class="type-item account" :class="{ active: discount.is_use_balance, disabled: balance == 0 }" @click="useBalance" v-if="payInfo.offset.balance">
<view class="iconfont iconyue"></view>
<view class="name" v-if="discount.is_use_balance">
余额支付
<text class="text">{{ payInfo.total_balance | moneyFormat }}</text>
</view>
<view class="name" v-else>
账户余额
<text class="text" v-if="balance > 0">{{ balance | moneyFormat }}</text>
</view>
<view class="iconfont iconxuanzhong"></view>
</view>
<view class="type-item account" :class="{ active: discount.is_use_point, disabled: payInfo.offset.point_array.point == 0 }" @click="usePoint" v-if="payInfo.offset.point_array">
<view class="iconfont iconjifen1"></view>
<view class="name" v-if="discount.is_use_point">
积分抵扣
<text class="text">{{ payInfo.point_money | moneyFormat }}{{ parseInt(payInfo.offset.point_array.point) }}积分</text>
</view>
<view class="name" v-else>
账户积分
<text class="text" v-if="globalMemberInfo.point">{{ globalMemberInfo.point }}积分</text>
</view>
<view class="iconfont iconxuanzhong"></view>
</view>
</view>
</block>
<view class="title">支付方式</view>
<view class="uni-flex pay-type">
<block v-for="(item, key,index) in payType" :key="key">
<view class="type-item" @click="switchPayType(item.type)" :class="{ active: item.type == type }">
<view class="pay-icon iconfont" :style="{ background: item.background }" :class="item.icon"></view>
<view class="name">{{ item.name }} [{{ item.hotKey }}]</view>
<view class="iconfont iconxuanzhong"></view>
</view>
</block>
<view class="type-item" @click="switchMemberCode()" :class="{ active: discount.is_use_balance}">
<view class="pay-icon iconfont iconhuiyuanma" :style="{ background: '#F7861E' }"></view>
<view class="name">
<text>会员码 [M]</text>
<template v-if="discount.is_use_balance">
<text style="margin-left: 0.05rem;">(</text>
<text style="margin-left: 0.05rem;">使用余额</text>
<text class="text">{{ payInfo.total_balance | moneyFormat }}</text>
<text style="margin-left: 0.05rem;">)</text>
</template>
</view>
<!-- <view class="iconfont iconxuanzhong"></view> -->
</view>
</view>
<view class="remark-info" v-if="payInfo.remark">备注{{ payInfo.remark }}</view>
</scroll-view>
<view class="button-wrap">
<view class="print-ticket">
<checkbox-group @change="autoPrintTicket = !autoPrintTicket">
<label>
<checkbox :checked="autoPrintTicket" style="transform:scale(0.7)" />
<text>打印小票</text>
</label>
</checkbox-group>
</view>
<button class="default-btn" @click="openRemark">备注</button>
<button class="default-btn cancel-btn" plain @click="cancelPayment">取消 [Esc]</button>
<button class="primary-btn" @click="confirm()" v-if="type != 'third' || payInfo.pay_money == 0">收款 [Enter]</button>
<button class="primary-btn" @click="thirdConfirm()" v-else>收款 [Enter]</button>
</view>
</view>
<scroll-view scroll-y="true" class="bill-wrap">
<view class="title">支付明细</view>
<view class="body">
<view class="bill-info">
<view>费用总额</view>
<view>{{ payInfo.original_money | moneyFormat }}</view>
</view>
<view class="block-title"><text>营销优惠</text></view>
<view class="bill-info">
<view>减免金额</view>
<view class="text">
-{{ payInfo.offset.reduction ? $util.moneyFormat(payInfo.offset.reduction) : '0.00' }}
</view>
</view>
<view class="bill-info" v-if="payInfo.offset.coupon_array">
<view>优惠券</view>
<view class="text">-{{ $util.moneyFormat(payInfo.coupon_money) }}</view>
</view>
<view class="bill-info" v-if="payInfo.offset.hongbao_array">
<view>红包</view>
<view class="text">-{{ $util.moneyFormat(payInfo.hongbao_money) }}</view>
</view>
<view class="bill-info" v-if="payInfo.offset.point_array">
<view>积分抵扣</view>
<view class="text">-{{ $util.moneyFormat(payInfo.point_money) }}</view>
</view>
<block v-if="payInfo.offset.balance">
<view class="block-title"><text>余额抵扣</text></view>
<view class="bill-info">
<view>余额支付</view>
<view>-{{ $util.moneyFormat(payInfo.total_balance) }}</view>
</view>
</block>
<view class="block-title"><text>支付方式</text></view>
<view class="bill-info">
<view>{{ payType[type].name }}</view>
<view v-show="type == 'cash'">
{{ payInfo.cash > 0 ? $util.moneyFormat(payInfo.cash) : $util.moneyFormat(payInfo.pay_money) }}
</view>
<view v-show="type != 'cash'">{{ payInfo.pay_money | moneyFormat }}</view>
</view>
<view class="block-title"></view>
<view class="bill-info">
<view>需支付</view>
<view>{{ payInfo.pay_money | moneyFormat }}</view>
</view>
<view class="bill-info">
<view>实付</view>
<view v-show="type == 'cash'">
{{ payInfo.cash > 0 ? $util.moneyFormat(payInfo.cash) : $util.moneyFormat(payInfo.pay_money) }}
</view>
<view v-show="type != 'cash'">{{ payInfo.pay_money | moneyFormat }}</view>
</view>
<view class="bill-info" v-if="payInfo.cash_change > 0">
<view>找零</view>
<view>{{ payInfo.cash_change | moneyFormat }}</view>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 支付结果 -->
<view class="uni-flex uni-column pay-result" v-show="payStatus == 'success'">
<view class="body status">
<view class="iconfont iconchenggong"></view>
<view class="msg">收款成功</view>
</view>
<view class="footer">
<button class="primary-btn" @click="paySuccess">继续收款 [Enter]{{ autoComplete.time }}s</button>
</view>
</view>
<uni-popup ref="moneyPopup" type="center">
<view class="money-wrap">
<view class="head">
<text>{{ moneyPopup.title }}</text>
<text class="iconfont iconguanbi1" @click="$refs.moneyPopup.close()"></text>
</view>
<view class="content-wrap">
<view class="unit"></view>
<view class="money">{{ moneyPopup.money }}</view>
</view>
<view class="keyboard-wrap">
<view class="num-wrap">
<view class="key-item" @click="keydown('1')">1</view>
<view class="key-item" @click="keydown('2')">2</view>
<view class="key-item" @click="keydown('3')">3</view>
<view class="key-item" @click="keydown('4')">4</view>
<view class="key-item" @click="keydown('5')">5</view>
<view class="key-item" @click="keydown('6')">6</view>
<view class="key-item" @click="keydown('7')">7</view>
<view class="key-item" @click="keydown('8')">8</view>
<view class="key-item" @click="keydown('9')">9</view>
<view class="key-item" @click="keydown('00')">00</view>
<view class="key-item" @click="keydown('0')">0</view>
<view class="key-item" @click="keydown('.')">.</view>
</view>
<view class="action-wrap">
<view class="delete" @click="deleteCode">删除</view>
<view class="delete" @click="moneyPopup.money = ''">清空</view>
<view class="confirm" @click="moneyPopupConfirm()">确认</view>
</view>
</view>
</view>
</uni-popup>
<uni-popup ref="couponPopup" type="center" v-if="payInfo.offset.coupon_array && payInfo.offset.coupon_array.member_coupon_list.length">
<view class="coupon-wrap">
<view class="head">
<text>选择优惠券</text>
<text class="iconfont iconguanbi1" @click="$refs.couponPopup.close()"></text>
</view>
<scroll-view scroll-y="true" class="body">
<view class="list">
<view class="item" :class="{ active: discount.coupon_id && discount.coupon_id == item.coupon_id }" v-for="(item, index) in payInfo.offset.coupon_array.member_coupon_list" :key="index" @click="selectCouponItem(item)">
<view class="money" v-show="item.type == 'discount'">
{{ item.discount }}
<text class="unit"></text>
</view>
<view class="money" v-show="item.type != 'discount'">
<text class="unit"></text>
{{ item.money }}
</view>
<view class="info">
<view class="title">{{ item.coupon_name }}</view>
<view class="limit">
{{ item.at_least == 0 ? '无门槛券' : '满' + item.at_least + '可用' }}
{{ item.type == 'discount' && item.discount_limit > 0 ? ',最多优惠' + item.discount_limit : '' }}
</view>
<view class="time" v-if="item.end_time">{{ $util.timeFormat(item.end_time, 'y-m-d') }}前可用
</view>
<view class="time" v-else>长期有效</view>
</view>
<view class="iconfont iconxuanzhong"></view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
<!-- 扫码枪支付弹窗 -->
<uni-popup ref="thirdPopup" type="center" @change="popupChange">
<view class="third-popup">
<view class="head">
<text>请选择扫码方式</text>
<text class="iconfont iconguanbi1" @click="$refs.thirdPopup.close();thirdPopupOpen = false;"></text>
</view>
<view class="money">扫码收款{{ payInfo.pay_money | moneyFormat }}</view>
<view class="scan-code-type" v-if="type == 'third'">
<view class="type-item" :class="{ active: scanCodeType == 'scancode' }" @click="scanCodeType = 'scancode'">扫码枪</view>
<view class="type-item" :class="{ active: scanCodeType == 'qrcode' }" @click="scanCodeType = 'qrcode'">二维码</view>
</view>
<view class="content-wrap">
<view class="qrcode-wrap" v-show="scanCodeType == 'qrcode'">
<block v-if="payQrcode.length">
<view class="qrcode-item" v-for="(item, index) in payQrcode" :key="index">
<image :src="item.qrcode.replace(/[\r\n]/g, '')" mode="widthFix" class="qrcode" v-if="item.qrcode.indexOf('data:image') != -1" />
<image :src="$util.img(item.qrcode)" mode="widthFix" class="qrcode" v-else />
<image :src="$util.img(item.logo)" mode="widthFix" class="logo" />
</view>
</block>
<view class="empty" v-else>没有可用的收款二维码</view>
</view>
<view class="scancode-wrap" v-show="scanCodeType == 'scancode'">
<block v-if="scancodeList.length">
<view>
<input type="number" v-model="authCode" :class="{ focus: scanCodeFocus }"
:focus="scanCodeFocus" placeholder="请点击输入框聚焦扫码或输入付款码" @confirm="scanCode"
@focus="scanCodeFocus = true" @blur="scanCodeInputBlur()" />
<text class="iconfont icondelete" v-show="authCode.length > 0" @click="clearAuthCode"></text>
</view>
<image src="@/static/cashier/scan_code_tip.png" mode="widthFix" />
</block>
<view class="empty" v-else>没有可用的支付方式</view>
</view>
</view>
</view>
</uni-popup>
<!-- 使用账号余额验证会员码/手机号 -->
<uni-popup ref="safeVerifyPopup" type="center">
<view class="safe-verify-popup">
<view class="header">
<view class="type-wrap" v-if="active == 'memberCodePopup'">
<view class="item">会员码</view>
</view>
<view class="type-wrap" v-else-if="active == 'safeVerifyPopup' && payInfo.collectmoney_config.sms_verify == 1">
<view class="item" :class="{ active: safeVerifyType == 'payment_code' }" @click="changeSafeVerifyType('payment_code')">会员码</view>
<view class="item" :class="{ active: safeVerifyType == 'sms_code' }" @click="changeSafeVerifyType('sms_code')">短信验证码</view>
</view>
<text class="iconfont iconguanbi1" @click="$refs.safeVerifyPopup.close()"></text>
</view>
<view class="content" v-show="safeVerifyType == 'payment_code'">
<view class="scancode-wrap">
<view class="input-wrap">
<view>
<input type="number" v-model="paymentCode" :class="{ focus: scanCodeFocus }"
:focus="scanCodeFocus" placeholder="请点击输入框聚焦扫码或输入会员码" @confirm="verifyPaymentCode"
@focus="scanCodeFocus = true" @blur="scanCodeInputBlur()"
placeholder-class="placeholder" />
<text class="iconfont icondelete" v-show="paymentCode.length > 0" @click="clearPaymentCode"></text>
</view>
<button class="primary-btn" @click="verifyPaymentCode">确认</button>
</view>
<image src="@/static/cashier/scan_code_tip.png" mode="widthFix" />
<!-- <view class="member-code-hint">打开手机端 --个人中心 -- 会员码</view> -->
</view>
</view>
<view class="content" v-show="safeVerifyType == 'sms_code' && active == 'safeVerifyPopup'">
<block v-if="payInfo.member_account">
<view class="tip">将发送验证码到该手机</view>
<view class="mobile">
{{ payInfo.member_account.mobile.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2') }}
</view>
<view class="sms-code">
<view>
<input type="number" v-model="smsCode" class="sms-code" placeholder="请输入验证码"
:focus="scanCodeFocus" placeholder-class="placeholder" @focus="scanCodeFocus = true"
@blur="scanCodeFocus = false" />
<text class="iconfont icondelete" v-show="smsCode.length > 0" @click="clearSmsCode"></text>
</view>
<text class="send-tip" @click="sendMobileCode" :class="{ disabled: dynacodeData.isSend }">{{ dynacodeData.codeText }}</text>
</view>
<button class="primary-btn" @click="verifySmsCode">确认</button>
</block>
<view v-else>该会员尚未绑定手机号无法使用该验证方式</view>
</view>
</view>
</uni-popup>
<uni-popup ref="remarkPopup" type="center">
<view class="remark-wrap">
<view class="header">
<text class="title">备注</text>
<text class="iconfont iconguanbi1" @click="$refs.remarkPopup.close()"></text>
</view>
<view class="body">
<textarea v-model="remark" placeholder="填写备注信息" placeholder-class="placeholder-class" @keydown.enter="remarkConfirm" />
</view>
<view class="footer">
<button type="default" class="primary-btn" @click="remarkConfirm">确认</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import index from './index.js';
export default {
name: 'nsPayment',
mixins: [index]
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@@ -0,0 +1,146 @@
import { editPendOrderRemark, deletePendOrder, getPendOrderList } from '@/api/pendorder.js';
import { getMemberInfoById } from '@/api/member.js'
import {mapGetters} from 'vuex';
export default {
name: 'nsPendOrder',
data() {
return {
orderData: {
page: 0,
total: 1,
list: []
},
remark: '',
index: -1,
orderId: 0,
isRepeat: false,
height: ''
};
},
computed: {
...mapGetters(['pendOrderNum'])
},
created() {
this.getOrder();
},
mounted() {
this.setHeight();
},
methods: {
open() {
this.$refs.pendOrderPop.open();
},
getOrder(page = null) {
if (page === 0) this.orderData.page = page;
this.orderId = 0;
if (this.orderData.page + 1 > this.orderData.total) return;
this.orderData.page++;
getPendOrderList({ page: this.orderData.page }).then(res => {
if (res.code == 0) {
if (this.orderData.page == 1) this.orderData.list = [];
this.$store.commit('billing/setPendOrderNum', res.data.count);
if (res.data.list.length) {
this.orderData.total = res.data.page_count;
this.orderData.list = this.orderData.list.concat(res.data.list);
} else {
this.orderData.total = 1;
}
this.setHeight();
}
});
},
deleteOrder(order_id) {
if (this.isRepeat) return;
this.isRepeat = true;
deletePendOrder(order_id).then(res => {
if (res.code == 0) {
this.isRepeat = false;
this.getOrder(0);
}
});
},
remarkConfirm() {
let data = this.orderData.list[this.index];
editPendOrderRemark({
order_id: data.order_id,
remark: this.remark
}).then(res => {
if (res.code == 0) {
this.orderData.list[this.index].remark = this.remark;
this.$refs.remarkPopup.close();
} else {
this.$util.showToast({
title: '操作失败'
});
}
})
},
remarkSetting(data, index) {
this.index = index;
this.remark = data.remark;
this.$refs.remarkPopup.open();
},
async takeOrder(data) {
this.orderId = data.order_id;
//获取挂单数据的会员信息
if (data.member_id) {
let res = await getMemberInfoById(data.member_id);
if (res.code == 0 && res.data) {
this.$store.commit('app/setGlobalMemberInfo', res.data);
} else {
this.$store.commit('app/setGlobalMemberInfo', null);
}
}
//取出挂单数据设置到展示列表
let goodsData = {};
data.order_goods.forEach(item => {
if (item.goods_class == 'money') item.money = item.price;
//item.is_adjust = true;
var key = 'sku_' + item.sku_id;
if (item.goods_class == 4 || item.goods_class == 6) {
var index = 0;
Object.keys(goodsData).forEach(k => {
if (k.indexOf(key) != -1) {
index++;
}
});
key += '_' + index;
}
goodsData[key] = item;
});
this.$store.commit('billing/setPendOrderId', data.order_id);
this.$store.commit('billing/setPendOrderNum', this.pendOrderNum - 1);
this.$store.commit('billing/setGoodsData', goodsData);
this.$store.commit('billing/setOrderData', {
goods_list: [],
remark: data.remark
});
this.$store.commit('billing/setActive', 'SelectGoodsAfter');
this.$refs.pendOrderPop.close();
},
switchStoreAfter() {
this.orderData = {
page: 0,
total: 1,
list: []
};
this.getOrder();
},
setHeight() {
this.$nextTick(() => {
const query = uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
query.selectViewport().scrollOffset(data => {
this.height = (data.scrollHeight - 51 - 67 - 15) / 100 + 'rem';
}).exec();
});
}
}
};

View File

@@ -0,0 +1,269 @@
.container {
height: 100%;
display: flex;
flex-direction: column;
.pend-order-scroll-view-wrap {
height: calc(100% - 0.67rem) !important;
box-sizing: border-box;
padding-top: 0.15rem;
padding-bottom: 0;
}
}
.header {
height: 0.66rem;
line-height: 0.66rem;
text-align: left;
border-bottom: 0.01rem solid #e6e6e6;
color: #303133;
font-size: 0.14rem;
}
.table-list {
.table-item {
border: 0.01rem solid #e6e6e6;
padding: 0.15rem;
position: relative;
margin-bottom: 0.2rem;
.table-header-info {
text {
font-size: 0.16rem;
}
.color {
font-size: 0.18rem;
color: #fe2278;
}
}
.table-header-time {
color: #909399;
font-size: 0.14rem;
margin-top: 0.1rem;
.line {
margin: 0 0.15rem;
}
}
.table-header-btn {
position: absolute;
right: 0.3rem;
top: 0.3rem;
color: $primary-color;
cursor: pointer;
}
.table-content {
margin-top: 0.1rem;
.table-content-item {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 0.01rem solid #e6e6e6;
padding: 0.1rem 0;
.content-item-left {
display: flex;
align-items: center;
flex: 1;
width: 0;
.content-item-info {
padding-right: 0.15rem;
flex: 1;
width: 0;
view {
font-size: 0.14rem;
}
.content-item-name {
margin-bottom: 0.05rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
white-space: normal;
word-break: break-all;
}
}
}
.content-item-img {
width: 0.5rem;
height: 0.5rem;
margin-right: 0.1rem;
image {
width: 100%;
height: 100%;
}
}
.content-item-number {
font-size: 0.16rem;
}
.content-item-price {
font-size: 0.14rem;
width: 40%;
text-align: right;
}
}
}
.remark-info {
padding: 0.1rem;
background-color: var(--primary-color-light-9);
color: $primary-color;
margin-top: 0.1rem;
font-size: 0.12rem;
}
.table-bottom {
margin-top: 0.2rem;
display: flex;
justify-content: end;
button {
width: 1rem;
margin: 0;
margin-right: 0.1rem;
}
}
}
}
/deep/ .uni-scroll-view {
&::-webkit-scrollbar {
width: 0.06rem;
height: 0.06rem;
background-color: rgba($color: #000000, $alpha: 0);
}
&::-webkit-scrollbar-button {
display: none;
}
&::-webkit-scrollbar-thumb {
border-radius: 0.06rem;
box-shadow: inset 0 0 0.06rem rgba(45, 43, 43, 0.45);
background-color: #ddd;
display: none;
}
&:hover::-webkit-scrollbar-thumb {
display: block;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
}
.remark-wrap {
width: 6rem;
background-color: #fff;
border-radius: 0.04rem;
box-shadow: 0 0.01rem 0.12rem 0 rgba(0, 0, 0, 0.1);
.header,
.footer {
height: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 0.01rem solid #e6e6e6;
position: relative;
.title {
font-size: 0.16rem;
font-weight: bold;
}
.iconfont {
line-height: 1;
position: absolute;
right: 0.15rem;
font-size: 0.2rem;
cursor: pointer;
}
}
.body {
padding: 0.15rem;
textarea {
border: 0.01rem solid #e6e6e6;
width: 100%;
padding: 0.1rem;
box-sizing: border-box;
font-size: 0.14rem;
}
.placeholder-class {
font-size: 0.14rem;
}
}
.footer {
border-top: 0.01rem solid #e6e6e6;
border-bottom: unset;
button {
width: 1rem;
}
}
}
.empty {
text-align: center;
padding-top: 1.2rem;
image {
width: 2rem;
}
.tips {
color: #999;
margin-top: 0.15rem;
}
}
// pop弹框
.pop-box {
background: #ffffff;
width: 8rem;
height: 7rem;
.pop-header {
padding: 0 0.15rem 0 0.2rem;
height: 0.5rem;
line-height: 0.5rem;
border-bottom: 0.01rem solid #f0f0f0;
font-size: 0.14rem;
color: #333;
overflow: hidden;
border-radius: 0.02rem 0.2rem 0 0;
box-sizing: border-box;
display: flex;
justify-content: space-between;
.pop-header-text {}
.pop-header-close {
cursor: pointer;
text {
font-size: 0.18rem;
}
}
}
}

View File

@@ -0,0 +1,95 @@
<template>
<view class="container pend-order">
<uni-popup ref="pendOrderPop">
<view class="pop-box">
<view class="pop-header">
<view class="pop-header-text">/取单</view>
<view class="pop-header-close" @click="$refs.pendOrderPop.close()">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<scroll-view scroll-y="true" @scrolltolower="getOrder" class="pend-order-scroll-view-wrap" :style="{height : height}">
<view class="table-list" v-if="orderData.list.length">
<block v-for="(item, index) in orderData.list" :key="index">
<view class="table-item" v-show="item.order_id != orderId">
<view class="table-header">
<view class="table-header-info">
<text>订单总价</text>
<text class="color">{{ item.order_money | moneyFormat }}</text>
</view>
<view class="table-header-time">
<text>挂单时间{{ item.create_time | timeFormat }}</text>
<block v-if="item.member_id">
<text class="line">|</text>
<text>会员{{ item.nickname }}</text>
</block>
</view>
</view>
<view class="table-content">
<view class="table-content-item" v-for="(goods, gindex) in item.order_goods" :key="gindex">
<view class="content-item-left">
<view class="content-item-img">
<image v-if="goods.goods_class == 'money'" src="@/static/goods/goods.png"/>
<image v-else-if="goods.goods_image == '@/static/goods/goods.png'" src="@/static/goods/goods.png"/>
<image v-else :src="$util.img(goods.goods_image, { size: 'small' })" @error="goods.goods_image = '@/static/goods/goods.png'"/>
</view>
<view class="content-item-info">
<view class="content-item-name" v-if="goods.goods_class == 'money'">无码商品</view>
<view class="content-item-name" v-else>
<text>{{ goods.goods_name }}</text>
<text>{{ goods.spec_name }}</text>
</view>
<view>{{ goods.price | moneyFormat }}</view>
</view>
</view>
<view class="content-item-number">x {{ goods.num }}</view>
<view class="content-item-price">{{ (goods.num * goods.price) | moneyFormat }}</view>
</view>
</view>
<view class="remark-info" v-if="item.remark">备注{{ item.remark }}</view>
<view class="table-bottom">
<button class="default-btn btn-left" @click="deleteOrder(item.order_id)">删除</button>
<button class="default-btn btn-left" @click="remarkSetting(item, index)">备注</button>
<button class="primary-btn btn-right" @click="takeOrder(item)">取单</button>
</view>
</view>
</block>
</view>
<view class="empty" v-if="!orderData.list.length || (orderData.list.length == 1 && orderId)">
<image src="@/static/goods/goods_empty.png" mode="widthFix"/>
<view class="tips">暂无挂单记录</view>
</view>
</scroll-view>
</view>
</uni-popup>
<uni-popup ref="remarkPopup" type="center">
<view class="remark-wrap">
<view class="header">
<text class="title">备注</text>
<text class="iconfont iconguanbi1" @click="$refs.remarkPopup.close()"></text>
</view>
<view class="body">
<textarea v-model="remark" placeholder="填写备注信息" placeholder-class="placeholder-class" />
</view>
<view class="footer">
<button type="default" class="primary-btn" @click="remarkConfirm">确认</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import index from './index.js';
export default {
mixins: [index]
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@@ -0,0 +1,127 @@
import {getAddonIsExist,getPromotionQrcode} from '@/api/promotion.js';
export default {
name: 'nsPromotionPopup',
props: {
pageName: {
type: String,
default: 'COUPON_DETAIL'
},
},
data() {
return {
qrParams:{
page_name:'',
option:'',
app_type:'h5'
},
APPType:'h5',
appTypeArray: [{
text: 'H5',
value: 'h5'
}],
qrData:{}
}
},
mounted() {
this.qrParams.page_name = this.pageName
this.getAddonIsExistFn()
},
methods:{
getAddonIsExistFn(){
getAddonIsExist().then(res=>{
if(res.data.weapp){
this.appTypeArray.push({text:'微信小程序',value:'weapp'})
}
if(res.data.aliapp){
this.appTypeArray.push({text:'支付宝小程序',value:'aliapp'})
}
})
},
getPromotionQrcodeFn(){
getPromotionQrcode(this.qrParams).then(res=>{
this.qrData = Object.assign(this.qrData,res.data)
this.$forceUpdate();
})
},
open(option){
this.qrParams.option = JSON.stringify(option)
this.$refs.promotionPop.open()
this.qrData={}
this.appTypeArray.forEach((el)=>{
this.qrParams.app_type = el.value
this.getPromotionQrcodeFn()
})
},
//复制链接
copyTextToClipboard(text) {
uni.setClipboardData({
data: text,
success: function () {
// 可以添加用户友好的提示例如使用uni.showToast提示复制成功
uni.showToast({
title: '复制成功',
icon: 'success',
duration: 2000
});
},
fail: function () {
console.log('复制失败');
// 可以添加错误处理或用户友好的提示
}
});
},
//下载二维码
download(url){
var oA = document.createElement("a");
oA.innerHTML = '123'
oA.download = ''; // 设置下载的文件名,默认是'下载'
oA.target = "_blank"
oA.href = url; //临时路径再保存到本地
document.body.appendChild(oA);
oA.click();
oA.remove(); // 下载之后把创建的元素删除
}
// download(url){
// //下载文档
// uni.downloadFile({
// url: url,//下载地址接口返回
// success: (data) => {
// if (data.statusCode === 200) {
// //文件保存到本地
// uni.saveFile({
// tempFilePath: data.tempFilePath, //临时路径
// success: function(res) {
// uni.showToast({
// icon: 'none',
// mask: true,
// title: '文件已保存:' + res.savedFilePath, //保存路径
// duration: 3000,
// });
// setTimeout(() => {
// //打开文档查看
// uni.openDocument({
// filePath: res.savedFilePath,
// success: function(res) {
// // console.log('打开文档成功');
// }
// });
// }, 3000)
// }
// });
// }
// },
// fail: (err) => {
// console.log(err);
// uni.showToast({
// icon: 'none',
// mask: true,
// title: '失败请重新下载',
// });
// },
// });
// }
}
}

View File

@@ -0,0 +1,76 @@
.promotion-pop{
width: 7rem;
background-color: #fff;
border-radius: 0.06rem;
.header{
padding: 0.15rem 0.2rem;
font-size: 0.14rem;
border-bottom: 0.01rem solid #e6e6e6;
}
.body{
width: 100%;
padding: 0.2rem 0.3rem;
box-sizing: border-box;
.alter{
height: 0.48rem;
line-height: 0.48rem;
font-size: 0.14rem;
padding: 0 0.2rem;
color: #666;
background-color: var(--primary-color-light-9);
margin-bottom: 0.2rem;
}
.content{
.qrCode{
width: 2rem;
height: 2rem;
background-color: #f8f8f8;
color: #333;
font-size: 0.14rem;
image{
width:1.6rem;
height:1.6rem;
}
}
.right{
margin-left: 0.2rem;
.form-item{
margin-bottom: 0.1rem;
}
.link {
.form-inline{
margin-top: 0.1rem;
}
}
input{
width: 2rem;
height: 0.3rem;
border: 0.01rem solid #e6e6e6;
padding: 0 0.12rem;
font-size: 0.14rem;
border-radius: 0.02rem;
box-sizing: border-box;
}
.btn{
background-color: var(--primary-color);
color: #fff;
margin-left: .1rem;
font-size: 0.14rem;
height: 0.3rem;
line-height: 0.3rem;
&::after{
border: 0;
}
}
.download{
color: var(--primary-color);
cursor: pointer;
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
<template>
<unipopup ref="promotionPop" type="center">
<view class="promotion-pop">
<view class="header flex justify-between">
<view class="title">推广</view>
<view class="pop-header-close" @click="$refs.promotionPop.close()">
<text class="iconguanbi1 iconfont"></text>
</view>
</view>
<view class="body">
<view class="alter">活动可分享至多个渠道推广增加曝光率提升分享打开率</view>
<view class="flex content">
<view class="qrCode flex items-center justify-center">
<image v-if="qrData[APPType]&&qrData[APPType].path" :src="$util.img(qrData[APPType].path)"/>
<text v-else>小程序配置错误</text>
</view>
<view class="flex-1 right">
<view class="form-box">
<view class="form-content">
<view class="form-item flex">
<view class="form-label">充值方式</view>
<view class="form-inline">
<uni-data-checkbox v-model="APPType" :localdata="appTypeArray" />
</view>
</view>
<view class="form-item link" v-if="APPType == 'h5'&&qrData[APPType]&&qrData[APPType].url">
<view class="form-label">
推广链接
</view>
<view class="form-inline flex items-center">
<input type="text" disabled v-model="qrData[APPType].url" @keydown.enter="search('enter')" />
<button type="default" class="btn" @click="copyTextToClipboard(qrData[APPType].url)">复制</button>
</view>
</view>
<view class="form-item" v-if="qrData[APPType]&&qrData[APPType].path">
<text class="download" @click="download($util.img(qrData[APPType].path))">下载二维码</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</unipopup>
</template>
<script>
import unipopup from '@/components/uni-popup/uni-popup.vue';
import index from './index.js';
export default {
components: {
unipopup,
},
mixins: [index]
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,384 @@
<template>
<view>
<uni-popup ref="memberInquirePopup" type="center" @change="popupChange" :mask-click="false">
<view class="popup-inquire-wrap">
<view class="popup-header">
<text class="title">称重商品同步到电子秤</text>
<text class="iconfont iconguanbi1" @click="$refs.memberInquirePopup.close()"></text>
</view>
<view class="popup-content">
<view v-show="step == 1">
<view class="content-title">选择商品</view>
<uniDataTable url="/weighgoods/storeapi/goods/skuall" :cols="cols" :classType="true" @checkBox="checkBox" ref="goodsListTable" />
</view>
<view v-show="step == 2">
<view class="content-title">选择需同步的电子秤</view>
<uniDataTable url="/scale/storeapi/scale/page" :pagesize="0" :cols="scaleCols" :classType="true" ref="scaleListTable" @tableData="onloadScale" />
</view>
<view v-show="step == 3">
<view class="content-title">同步商品到电子秤</view>
<uniDataTable :cols="syncTaskCols" :classType="true" ref="syncTaskTable" :data="syncTask" />
</view>
</view>
<view class="popup-footer" v-show="step == 1">
<button type="default" class="default-btn" @click="next">下一步</button>
</view>
<view class="popup-footer" v-show="step == 2">
<button type="default" class="default-btn" @click="step = 1">上一步</button>
<button type="primary" class="primary-btn" @click="syncGoods">同步</button>
</view>
<view class="popup-footer" v-show="step == 3">
<button type="default" class="primary-btn" :loading="synching" v-if="synching">同步中</button>
<button type="default" class="primary-btn" :loading="synching" @click="$refs.memberInquirePopup.close()" v-else>完成</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import uniDataTable from '@/components/uni-data-table/uni-data-table.vue';
import { getScaleList } from '@/api/scale.js';
import {
createCommand
} from './command.js'
var self;
export default {
components: {
uniDataTable
},
data() {
return {
step: 1,
cols: [{
width: 6,
align: 'center',
checkbox: true
},
{
field: 'account_data',
width: 50,
title: '商品信息',
align: 'left',
templet: data => {
let img = this.$util.img(data.sku_image);
let html = `
<view class="goods-content">
<image class="goods-img" src="${img}" mode="aspectFit"/>
<text class="goods-name multi-hidden">${data.sku_name}</text>
</view>
`;
return html;
}
},
{
width: 10,
title: '价格',
align: 'center',
templet: function(data) {
return '¥' + data.price;
}
},
{
field: 'plu',
width: 10,
title: 'PLU码',
align: 'center'
},
{
width: 10,
title: '计价方式',
align: 'center',
templet: function(data) {
if (data.pricing_type == 'num') {
return '计数';
} else {
return '计重';
}
}
},
{
width: 10,
title: '状态',
align: 'center',
templet: function(data) {
var str = '';
if (data.store_status == 1) {
str = '销售中';
} else if (data.store_status == 0) {
str = '仓库中';
}
return str;
}
}
],
scaleCols: [{
width: 6,
align: 'center',
checkbox: true,
disabled: (data) => {
return !data.connect_status
}
},
{
width: 24,
title: '设备名称',
align: 'left',
field: 'name',
},
{
width: 20,
title: '设备品牌',
align: 'center',
field: 'brand_name',
},
{
width: 20,
title: '设备型号',
align: 'center',
field: 'model_name',
},
{
width: 20,
title: '状态',
align: 'center',
templet: (data) => {
return data.connect_status ? '已连接' : '未连接'
}
}
],
syncTaskCols: [{
width: 70,
title: '设备名称',
align: 'left',
templet: (data) => {
return data.name
}
},
{
width: 30,
title: '同步状态',
align: 'left',
templet: (data) => {
var str = '';
switch (data.syncStatus) {
case '1':
str = `<view>同步成功</view>`;
break;
case '0':
str = `<view>同步失败</view><view style="color: red;display:block;white-space: normal;">失败原因:${data.msg}</view>`;
break;
default:
str = `<view>同步中</view>`;
break;
}
return str;
}
}
],
syncTask: {},
synching: false
};
},
created() {
this.getScaleListFn();
self = this;
},
methods: {
popupChange(e) {
if (!e.show) {
this.syncTask = {};
this.synching = false;
this.step = 1
}
},
open() {
this.$refs.memberInquirePopup.open();
},
selectScale(e) {
this.scale = this.scaleList[e].name;
this.scaleId = this.scaleList[e].scale_id
},
checkBox(e) {
this.goodsList = e;
},
getScaleListFn() {
if (!this.addon.includes('scale')) {
return;
}
getScaleList({
page: 1,
page_size: 100
}).then(res=>{
if (res.data.list.length > 0) this.scaleList = res.data.list;
});
},
next() {
const selected = this.$refs.goodsListTable.selected;
if (!selected.length) {
this.$util.showToast({
'title': '请选择要同步的商品'
});
return
}
this.step += 1;
},
onloadScale(list) {
if (typeof window.POS_DATA_CALLBACK == 'function') delete window.POS_DATA_CALLBACK;
/**
* 商品同步数据回调
* @param {Object} text
*/
window.POS_DATA_CALLBACK = function(text) {
let data = text.split(':');
let index = parseInt(data[0]);
switch (data[1]) {
case 'SyncGoodsPlu':
self.$set(self.syncTask[index], 'syncStatus', data[2]);
self.$set(self.syncTask[index], 'msg', data[4]);
if (index == self.syncTask.length - 1) {
self.synching = false
}
break;
case 'PingWeigher':
self.$set(self.$refs.scaleListTable.list[index], 'connect_status', parseInt(data[3]));
break;
}
};
let weigher = list.map(item => {
item.config = JSON.parse(item.config);
return item;
});
try {
this.$pos.send('PingWeigher', JSON.stringify({
weigher
}));
} catch (e) {}
},
async syncGoods() {
const selected = this.$refs.scaleListTable.selected;
if (!selected.length) {
this.$util.showToast({
'title': '请选择要同步的设备'
});
return;
}
if (this.synching) return;
this.synching = true;
this.syncTask = this.$refs.scaleListTable.selected
this.step += 1;
setTimeout(() => {
this.createSyncData()
}, 100)
},
createSyncData() {
let task = {};
task.weigher = this.$refs.scaleListTable.selected.map(scale => {
scale.config = typeof scale.config == 'string' ? JSON.parse(scale.config) : scale.config;
scale.goodsList = this.$refs.goodsListTable.selected.map(sku => {
return {
sku_no: sku.sku_no,
plu: sku.plu,
price: sku.price,
sku_name: sku.sku_name,
pricing_type: sku.pricing_type,
command: ''
}
});
return scale;
});
this.syncTask = task.weigher;
try {
console.log(JSON.stringify(task));
this.$pos.send('SyncGoodsPlu', JSON.stringify(task));
} catch (e) {
this.synching = false
}
}
}
};
</script>
<style lang="scss" scoped>
.popup-inquire-wrap {
overflow: hidden;
width: 9.55rem;
height: 5.37rem;
background-color: #fff;
border-radius: 0.05rem;
display: flex;
flex-direction: column;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0.15rem;
height: 0.45rem;
line-height: 0.45rem;
border-bottom: 0.01rem solid #e8eaec;
.iconfont {
font-size: $uni-font-size-lg;
}
}
.popup-content {
flex: 1;
height: 0;
overflow-y: auto;
padding: 0.1rem;
/deep/ .content {}
.content-title {
margin-bottom: .1rem;
}
}
.popup-footer {
padding: 0.1rem;
display: flex;
justify-content: end;
button {
width: 1rem;
margin: 0 0 0 .15rem;
}
}
}
/deep/.goods-content {
display: flex;
.goods-img {
margin-right: 0.1rem;
width: 0.5rem;
height: 0.5rem;
}
.goods-name {
white-space: pre-wrap;
align-self: baseline;
}
}
/deep/.uni-select-lay-select {
width: 2rem !important;
height: 0.42rem !important;
}
</style>

View File

@@ -0,0 +1,242 @@
import {getMemberList,getMemberInfoById, getMemberLevelList, addMember, searchMemberByMobile} from '@/api/member.js';
import {mapGetters} from 'vuex';
export default {
data() {
return {
searchText: '',
page: 1,
memberList: [],
memberId: '',
memberData: {
sex: 0,
mobile: '',
nickname: '',
birthday: '',
member_level: '',
member_level_name: ''
},
memberLevelList: [], // 会员等级
sex: [{
text: '未知',
value: 0
}, {
text: '男',
value: 1
}, {
text: '女',
value: 2
}],
memberType: 'login',
flag: false,
inputFocus: false,
isPhone: false,
searchFinish: false // 搜索是否完成
};
},
created() {
this.getMemberLevel();
},
computed: {
...mapGetters(['memberSearchWayConfig'])
},
watch: {
memberSearchWayConfig: {
immediate: true,
handler(newVal, oldVal) {
if(newVal) {
if(newVal.way == 'list'){
this.getMemberListFn();
}
}
}
}
},
methods: {
searchMemberInputBlur(){
this.inputFocus = false;
//强制聚焦处理
if(this.memberType = 'login'){
this.$nextTick(() => {
this.inputFocus = true;
});
}
},
open(callback) {
this.memberId = this.globalMemberInfo ? this.globalMemberInfo.member_id + '' : '';
this.$refs.memberPopup.open('', callback);
this.inputFocus = true;
this.searchFinish = false;
},
// 查询会员列表
searchMemberByMobileFn() {
setTimeout(() => {
if (!this.searchText) return false;
searchMemberByMobile({
mobile: this.searchText
}).then((res) => {
if (res.code >= 0) {
this.$store.commit('app/setGlobalMemberInfo', res.data);
this.initData();
this.$refs.memberPopup.close();
} else {
if (res.data > 1) {
this.$util.showToast({
title: res.message
});
return false;
}
var regex = /^1[3-9]\d{9}$/;
if (res.data == 0 && regex.test(this.searchText)) {
this.isPhone = true;
this.$refs.emptyPopup.open();
return false;
}
if (res.data == 0) {
this.isPhone = false;
this.$refs.emptyPopup.open();
return false;
}
}
});
}, 200)
},
getMemberInfo(memberId, callback) {
this.memberId = memberId;
getMemberInfoById(memberId).then(res => {
if (res.code == 0 && res.data) {
this.$store.commit('app/setGlobalMemberInfo', res.data);
if (callback) callback();
this.initData();
this.$refs.memberPopup.close();
} else {
this.$util.showToast({
title: '未获取到会员信息'
});
}
})
},
/******************************** 录入会员 ********************************/
getMemberLevel() {
this.memberLevelList = [];
getMemberLevelList().then(res => {
if (res.code == 0 && res.data) {
for (let i in res.data) {
this.memberLevelList.push({
label: res.data[i]['level_name'],
value: res.data[i]['level_id'].toString(),
disabled: false
});
}
}
});
},
// 选择会员等级
selectMemberLevel(index, item) {
if (index >= 0) {
this.memberData.member_level = item.value;
this.memberData.member_level_name = item.label;
} else {
this.memberData.member_level = '';
this.memberData.member_level_name = '';
}
this.$forceUpdate();
},
// 选择时间
changeTime(e) {
this.memberData.birthday = e;
},
verify() {
if (!this.memberData.mobile) {
this.$util.showToast({
title: '请输入会员手机号'
});
return false;
}
if (!this.$util.verifyMobile(this.memberData.mobile)) {
this.$util.showToast({
title: '请输入正确的手机号码'
});
return false;
}
return true;
},
// 确定录入
addMemberFn() {
if (this.verify()) {
if (this.flag) return;
this.flag = true;
addMember(this.memberData).then(res => {
if (res.code == 0 && res.data) {
this.memberType = 'login';
this.getMemberInfo(res.data)
} else {
this.$util.showToast({
title: res.message
});
}
this.flag = false;
})
}
},
closedFn() {
this.memberType = "login";
this.$refs.memberPopup.close();
},
memberEmptyRegister() {
this.memberType = "register";
this.memberData.mobile = this.searchText;
this.$refs.emptyPopup.close();
},
initData() {
this.searchText = '';
this.memberData.sex = 0;
this.memberData.mobile = '';
this.memberData.nickname = '';
this.memberData.birthday = '';
this.memberData.member_level = '';
this.memberData.member_level_name = '';
},
stayTuned() {
this.$util.showToast({
title: '敬请期待'
});
},
getMemberListFn(isSearch){
getMemberList({
page: this.page,
page_size: 12,
search_text: this.searchText
}).then((res)=>{
if (res.code >= 0) {
if (this.page == 1) this.memberList = [];
this.memberList = this.memberList.concat(res.data.list);
this.memberList.forEach((item) => {
if (item.mobile) {
if (this.userInfo && this.userInfo.is_admin == 0) {
// 非管理员,不能查看会员手机号
item.mobile = item.mobile.substring(0, 4 - 1) + '****' + item.mobile.substring(6 + 1);
}
} else {
item.mobile = '--';
}
});
if (isSearch) {
// 默认选中第一个搜索结果
this.memberId = 0;
if (this.memberList.length) {
this.memberId = this.memberList[0].member_id;
}
}
this.searchFinish = true;
if (res.data.page_count >= this.page) this.page++;
}
})
},
searchMemberByList(){
this.page = 1;
this.getMemberListFn(Boolean(this.searchText));
}
}
};

View File

@@ -0,0 +1,333 @@
.member-inquire-wrap {
overflow: hidden;
background-color: #fff;
border-radius: 0.05rem;
.member-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0.15rem;
height: 0.45rem;
line-height: 0.45rem;
border-bottom: 0.01rem solid #e8eaec;
.iconfont {
font-size: $uni-font-size-lg;
}
}
&.exact{
width: 4rem;
.member-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.3rem 0.3rem;
.member-img{
width: 0.75rem;
height: 0.75rem;
}
.member-input{
margin-top: .25rem;
width: 100%;
height: .4rem;
line-height: .4rem;
// border-radius: 0.02rem;
padding: 0 0.1rem;
border: 0.01rem solid $primary-color;
box-sizing: border-box;
text-align: center;
}
button{
// border-radius: 0.05rem;
height: .4rem;
line-height: .4rem;
margin-top: 0.15rem;
width: 100%;
}
.function-list{
margin-top: .25rem;
padding-top: .15rem;
border-top: 0.01rem dashed #ccc;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.item-wrap{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
.item-icon{
font-size: .25rem;
color: #333;
margin-bottom: 0.05rem;
}
}
}
}
}
&.list{
width: 9.55rem;
height: 5.37rem;
.member-content {
padding: 0.1rem;
.search-warp {
margin-left: 0.1rem;
.search-input {
display: flex;
align-items: center;
margin-bottom: 0.05rem;
input {
flex: 1;
padding-left: 0.1rem;
width: 2.5rem;
height: 0.4rem;
line-height: 0.4rem;
border: 0.01rem solid #dcdee2;
border-right: none;
}
button {
width: 2rem;
height: 0.42rem;
line-height: 0.42rem;
color: #fff;
font-size: $uni-font-size-base;
margin: 0;
&:last-of-type {
margin-left: 0.15rem;
margin-right: 0.1rem;
}
}
}
}
/deep/ .uni-scroll-view-content {
display: flex;
flex-wrap: wrap;
align-content: baseline;
height: 430px;
}
.member-list {
.member-item {
display: flex;
padding: 0.13rem 0.15rem;
margin: 0.1rem;
width: 2.9rem;
height: 1rem;
background-color: #f5f5f5;
box-sizing: border-box;
border-radius: 0.05rem;
&.active {
background-color: $primary-color;
.name,
.phone,
.other>view {
color: #fff;
}
}
&:nth-child(3n + 3) {
margin-right: 0;
}
image {
width: 0.45rem;
height: 0.45rem;
border-radius: 50%;
margin-right: 0.1rem;
flex-shrink: 0;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
width: calc( 100% - 0.55rem );
.name{
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.name {
text:nth-child(1) {
font-size: $uni-font-size-lg;
font-weight: bold;
}
}
.phone {
font-size: $uni-font-size-sm;
color: #666;
}
.other {
display: flex;
justify-content: space-between;
font-size: $uni-font-size-sm;
view {
font-size: $uni-font-size-sm;
color: #666;
}
}
}
}
}
}
}
// 录入会员
.member-entering-wrap {
width: 3.8rem;
background-color: #fff;
border-radius: 0.05rem;
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0.15rem;
height: 0.45rem;
line-height: 0.45rem;
border-bottom: 0.01rem solid #e8eaec;
.iconfont {
font-size: $uni-font-size-lg;
}
}
.form-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.2rem;
.form-item {
margin-bottom: 0.1rem;
display: flex;
.form-label {
width: 0.9rem;
text-align: right;
padding-right: 0.1rem;
box-sizing: border-box;
height: 0.32rem;
line-height: 0.32rem;
.required {
color: red;
margin-right: 0.03rem;
}
}
.form-inline {
width: 2.5rem;
line-height: 0.32rem;
box-sizing: border-box;
.form-input {
border-width: 0.01rem;
border-style: solid;
background-color: #fff;
color: rgba(0, 0, 0, 0.85);
border-radius: 0.02rem;
padding-left: 0.1rem;
height: 0.32rem;
line-height: 0.32rem;
font-size: 0.14rem;
border-color: #e6e6e6;
}
button {
width: calc(50% - 0.05rem);
display: inline-block;
margin-right: 0.1rem;
&:nth-child(2) {
margin-right: 0;
}
}
}
}
.btn-wrap {
width: 100%;
box-sizing: border-box;
padding: 0.1rem 0;
.primary-btn {
height: 0.4rem;
line-height: 0.4rem;
}
}
}
}
.empty {
text-align: center;
padding-top: 0.8rem;
width: 100%;
image {
width: 2rem;
}
.tips {
color: #999;
margin-top: 0.15rem;
}
}
.member-empty{
overflow: hidden;
background-color: #fff;
border-radius: 0.02rem;
width: 3rem;
.head{
display: flex;
align-items: center;
padding-left: .2rem;
height: .42rem;
background-color: #f8f8f8;
border-bottom: .01rem solid #eee;
}
.content{
padding: .06rem .2rem 0;
height: .6rem;
line-height: .6rem;
}
.btn-wrap{
display: flex;
justify-content: flex-end;
padding-right: .2rem;
padding-bottom: .2rem;
button{
margin: 0;
margin-left: .1rem;
border-radius: .02rem;
}
.close-btn{
font-size: 0.14rem;
}
}
}

View File

@@ -0,0 +1,154 @@
<template>
<view>
<uni-popup ref="memberPopup" type="center" @maskClick="closedFn">
<view class="member-inquire-wrap" :class="{ 'exact' : memberSearchWayConfig.way == 'exact','list' : memberSearchWayConfig.way == 'list' }" v-if="memberType == 'login'">
<view class="member-header">
<text class="title">{{ memberSearchWayConfig.way == 'exact' ? '会员查询' : '会员列表' }}</text>
<text class="iconfont iconguanbi1" @click="closedFn"></text>
</view>
<view class="member-content" v-if="memberSearchWayConfig.way == 'exact'">
<image class="member-img" mode="aspectFill" src="@/static/member/head.png" />
<input type="number" class="member-input" focus placeholder="请输入手机号或手机号后四位" placeholder-style="font-size:0.14rem" v-model="searchText" @confirm="searchMemberByMobileFn()" :focus="inputFocus" @focus="inputFocus = true" @blur="searchMemberInputBlur" />
<button class="switch primary-btn" @click="searchMemberByMobileFn()">查询</button>
<view class="function-list">
<view class="item-wrap" @click="stayTuned">
<text class="item-icon iconfont iconmenpos"></text>
<text>刷卡登录</text>
</view>
<view class="item-wrap" @click="stayTuned">
<!-- <image class="item-img" mode="aspectFill" src="@/static/member/head.png" /> -->
<text class="item-icon iconfont iconsaomiaoerweima"></text>
<text>扫码登录</text>
</view>
<view class="item-wrap" @click="stayTuned">
<text class="item-icon iconfont iconhuaxiangfenxi"></text>
<text>人脸登录</text>
</view>
<view class="item-wrap" @click="memberType = 'register'">
<text class="item-icon iconfont iconhuiyuanzhucedengluguanli"></text>
<text>会员注册</text>
</view>
</view>
</view>
<view class="member-content" v-if="memberSearchWayConfig.way == 'list'">
<view class="search-warp">
<view class="search-input">
<input focus placeholder="可查询会员账号、手机号、昵称" placeholder-style="font-size:0.14rem" v-model="searchText" @confirm="searchMemberByList()" :focus="inputFocus" @focus="inputFocus = true" @blur="searchMemberInputBlur" />
<button class="switch primary-btn" @click="searchMemberByList()">查询 [Enter]</button>
<button class="default-btn" plain="true" @click="memberType = 'register'">添加会员</button>
</view>
</view>
<scroll-view @scrolltolower="getMemberListFn()" scroll-y="true" class="member-list">
<view :class="['member-item', { active: item.member_id == memberId }]" v-for="(item, index) in memberList" :key="index" @click="getMemberInfo(item.member_id)">
<image class="item-img" mode="aspectFill" v-if="item.headimg" :src="$util.img(item.headimg)" @error="item.headimg = defaultImg.head"/>
<image class="item-img" mode="aspectFill" v-else :src="$util.img(defaultImg.head)"/>
<view class="item-content">
<view class="name">
<text :title="item.nickname">{{ item.nickname }}</text>
</view>
<view class="phone">手机号{{ item.mobile }}</view>
<view class="other">
<view>余额{{ parseFloat(parseFloat(item.balance) + parseFloat(item.balance_money)).toFixed(2) }}</view>
</view>
</view>
</view>
<view v-show="memberList.length == 0" class="empty">
<image :src="$util.img('public/uniapp/cashier/member-empty.png')" mode="widthFix"/>
<view class="tips">暂无会员</view>
</view>
</scroll-view>
</view>
</view>
<view class="member-entering-wrap" v-if="memberType == 'register'">
<view class="header">
<text class="iconfont iconqianhou1" @click="memberType = 'login'"></text>
<text class="title">录入会员</text>
<text class="iconfont iconguanbi1" @click="closedFn"></text>
</view>
<view class="form-content">
<view>
<view class="form-item">
<view class="form-label">
<text class="required">*</text>
手机号
</view>
<view class="form-inline">
<input type="number" class="form-input" v-model="memberData.mobile" placeholder="请输入会员手机号" />
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
会员名称
</view>
<view class="form-inline">
<input type="text" class="form-input" v-model="memberData.nickname" placeholder="请输入会员昵称" />
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
性别
</view>
<view class="form-inline">
<uni-data-checkbox v-model="memberData.sex" :localdata="sex"/>
</view>
</view>
<view class="form-item">
<view class="form-label">
<text class="required"></text>
生日
</view>
<view class="form-inline">
<uni-datetime-picker v-model="memberData.birthday" type="date" :clearIcon="false" @change="changeTime" />
</view>
</view>
<view class="form-item" v-if="memberLevelList.length">
<view class="form-label">
<text class="required"></text>
会员等级
</view>
<view class="form-inline">
<select-lay :zindex="10" :value="memberData.member_level" name="names" placeholder="请选择会员等级" :options="memberLevelList" @selectitem="selectMemberLevel"/>
</view>
</view>
</view>
<view class="btn-wrap">
<button type="primary" class="primary-btn" @click="addMemberFn">确定录入</button>
</view>
</view>
</view>
</uni-popup>
<uni-popup ref="emptyPopup" type="center">
<view class="member-empty">
<view class="head">提示</view>
<view class="content">未找到顾客{{searchText}}</view>
<view class="btn-wrap">
<button class="close-btn" @click="$refs.emptyPopup.close()">关闭</button>
<button class="primary-btn" v-if="isPhone" @click="memberEmptyRegister()">注册</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import index from './index.js';
export default {
mixins: [index]
};
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@@ -0,0 +1,146 @@
<template>
<view>
<uni-popup ref="updatePopup" type="center" :maskClick="false" v-if="versionInfo">
<view class="update-wrap">
<view class="head"><image src="@/static/cashier/update_header.png" /></view>
<view class="body">
<view class="version-no">版本号{{ versionInfo.version }}</view>
<view class="title">更新内容</view>
<view class="desc common-scrollbar">{{ versionInfo.update_desc }}</view>
<button type="default" class="primary-btn" @click="update">立即更新</button>
<view class="giveup-update" @click="giveupUpdate" v-if="!versionInfo.is_force_upgrade">以后再说</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
/**
* app版本更新
*/
import {checkUpdate} from '@/api/config.js'
export default {
data() {
return {
versionInfo: null
};
},
created() {
// wifi模式下才检测升级
if (plus.networkinfo.getCurrentType() == plus.networkinfo.CONNECTION_WIFI) {
this.checkUpdateFn();
}
},
methods: {
/**
* 检测是否有新版本
*/
checkUpdateFn() {
checkUpdate({
app_key: this.$config.app.app_key,
version: this.$config.app.version_no,
platform: uni.getSystemInfoSync().platform
}).then(res=>{
if (res.code == 0 && res.data) {
this.versionInfo = res.data;
if (!uni.getStorageSync('version_' + this.versionInfo.version_no)) {
this.$refs.updatePopup.open();
}
}
})
},
/**
* 确认更新
*/
update() {
let systemInfo = uni.getSystemInfoSync();
if (systemInfo.platform == 'android') {
uni.showLoading({});
uni.downloadFile({
url: this.$util.img(this.versionInfo.package_path),
success: data => {
uni.hideLoading();
if (data.statusCode === 200) {
plus.runtime.install(
data.tempFilePath,
{
force: false
},
function() {
plus.runtime.restart();
}
);
}
},
fail: res => {
this.$util.showToast({ title: '安装包下载失败' });
uni.hideLoading();
}
});
} else if (systemInfo.platform == 'ios') {
plus.runtime.launchApplication({ action: this.versionInfo.package_path }, e => {
this.$util.showToast({ title: e.message });
this.$refs.updatePopup.close();
});
}
},
/**
* 放弃本次更新
*/
giveupUpdate() {
uni.setStorageSync('version_' + this.versionInfo.version_no, 1);
this.$refs.updatePopup.close();
}
}
};
</script>
<style lang="scss" scoped>
.update-wrap {
width: 3rem;
.head {
height: 0.98rem;
image {
width: 3rem;
height: 0.98rem;
}
}
.body {
padding: 0.2rem 0.3rem;
background: #fff;
.version-no {
margin-bottom: 0.15rem;
}
.desc {
max-height: 1rem;
}
.title {
font-size: 0.16rem;
font-weight: 700;
margin-bottom: 0.15rem;
}
.primary-btn {
margin-top: 0.15rem;
}
.giveup-update {
margin-top: 0.15rem;
text-align: center;
line-height: 1;
}
}
}
/deep/ .uni-popup {
z-index: 1010;
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<view class="pick-regions">
<picker mode="multiSelector" :value="multiIndex" :range="multiArray" @change="handleValueChange" @columnchange="handleColumnChange">
<slot></slot>
</picker>
</view>
</template>
<script>
import {getAreaList} from '@/api/address.js'
export default {
props: {
defaultRegions: {
type: Array
},
selectArr: {
type: String
}
},
data() {
return {
pickerValueArray: [],
cityArr: [],
districtArr: [],
multiIndex: [0, 0, 0],
isInitMultiArray: false,
// 是否加载完默认地区
isLoadDefaultAreas: false
};
},
watch: {
defaultRegions: {
handler(arr, oldArr = []) {
// 避免传的是字面量的时候重复触发
if (arr.length != this.selectArr || arr.join('') === oldArr.join('')) return;
this.handleDefaultRegions();
},
immediate: true
}
},
computed: {
multiArray() {
if (!this.isLoadDefaultAreas) return;
var arr = this.pickedArr.map(arr => arr.map(item => item.label));
return arr;
},
pickedArr() {
// 进行初始化
if (this.isInitMultiArray) {
if (this.selectArr == '2') {
return [this.pickerValueArray[0], this.pickerValueArray[1]];
} else {
return [this.pickerValueArray[0], this.pickerValueArray[1], this.pickerValueArray[2]];
}
}
if (this.selectArr == '2') {
return [this.pickerValueArray[0], this.cityArr];
} else {
return [this.pickerValueArray[0], this.cityArr, this.districtArr];
}
}
},
created() {
this.getDefaultAreas(0, { level: 0 });
},
methods: {
async handleColumnChange(e) {
this.isInitMultiArray = false;
let col = e.detail.column;
let row = e.detail.value;
this.multiIndex[col] = row;
switch (col) {
case 0:
//选择省,加载市、区县
this.cityArr = await this.getAreasAsync(this.pickerValueArray[0][this.multiIndex[col]].value);
this.districtArr = await this.getAreasAsync(this.cityArr[0].value);
break;
case 1:
//选择市,加载区县
this.districtArr = await this.getAreasAsync(this.cityArr[this.multiIndex[col]].value);
break;
case 2:
if (!this.cityArr.length) this.cityArr = await this.getAreasAsync(this.pickerValueArray[0][0].value)
if (!this.districtArr.length) this.districtArr = await this.getAreasAsync(this.cityArr[0].value);
break;
}
},
handleValueChange(e) {
// 结构赋值
let [index0, index1, index2] = e.detail.value;
let [arr0, arr1, arr2] = this.pickedArr;
let address = '';
if (this.selectArr == '2') {
address = [arr0[index0], arr1[index1]];
} else {
address = [arr0[index0], arr1[index1], arr2[index2]];
}
this.$emit('getRegions', address);
},
handleDefaultRegions() {
var time = setInterval(() => {
if (!this.isLoadDefaultAreas) return;
this.isInitMultiArray = false;
for (let i = 0; i < this.defaultRegions.length; i++) {
for (let j = 0; j < this.pickerValueArray[i].length; j++) {
// 匹配省
if ( (this.defaultRegions[i] == this.pickerValueArray[i][j].value || this.defaultRegions[i] == this.pickerValueArray[i][j].label) && this.pickerValueArray[i][j].level == 1) {
// 设置选中省
this.$set(this.multiIndex, i, j);
// 查询市
this.getAreas(this.pickerValueArray[i][j].value, data => {
this.cityArr = data;
for (let k = 0; k < this.cityArr.length; k++) {
if (this.defaultRegions[1] == this.cityArr[k].value || this.defaultRegions[1] == this.cityArr[k].label) {
// 设置选中市
this.$set(this.multiIndex, 1, k);
// 查询区县
this.getAreas(this.cityArr[k].value, data => {
this.districtArr = data;
// 设置选中区县
for (let u = 0; u < this.districtArr.length; u++) {
if (this.defaultRegions[2] == this.districtArr[u].value || this.defaultRegions[2] == this.districtArr[u].label) {
this.$set(this.multiIndex, 2, u);
this.handleValueChange({
detail: {
value: [j, k, u]
}
});
break;
}
}
});
break;
}
}
});
}
}
}
if (this.isLoadDefaultAreas) clearInterval(time);
}, 100);
},
getDefaultAreas(pid, obj) {
getAreaList({ pid: pid }).then(res=>{
if (res.code == 0) {
var data = [];
var selected = undefined;
res.data.forEach((item, index) => {
if (obj != undefined) {
if (obj.level == 0 && obj.province_id != undefined) {
selected = obj.province_id;
} else if (obj.level == 1 && obj.city_id != undefined) {
selected = obj.city_id;
} else if (obj.level == 2 && obj.district_id != undefined) {
selected = obj.district_id;
}
}
if (selected == undefined && index == 0) {
selected = item.id;
}
data.push({
value: item.id,
label: item.name,
level: item.level
});
});
this.pickerValueArray[obj.level] = data;
if (obj.level + 1 < 3) {
obj.level++;
this.getDefaultAreas(selected, obj);
} else {
this.isInitMultiArray = true;
this.isLoadDefaultAreas = true;
}
}
});
},
// 同步获取地区
async getAreasAsync(pid) {
let res = await getAreaList({ pid: pid });
if (res.code == 0) {
var data = [];
res.data.forEach((item, index) => {
data.push({
value: item.id,
label: item.name,
level: item.level
});
});
return data;
}
},
// 异步获取地区
getAreas(pid, callback) {
getAreaList({ pid: pid }).then(res=>{
if (res.code == 0) {
var data = [];
res.data.forEach((item, index) => {
data.push({
value: item.id,
label: item.name,
level: item.level
});
});
if (callback) callback(data);
}
})
}
}
};
</script>

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,166 @@
<template>
<view class="container loading1">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading1',
data() {
return {};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading1 {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading1 .shape1 {
-webkit-animation: animation1shape1 0.5s ease 0s infinite alternate;
animation: animation1shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, 16px);
transform: translate(16px, 16px);
}
}
@keyframes animation1shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, 16px);
transform: translate(16px, 16px);
}
}
.loading1 .shape2 {
-webkit-animation: animation1shape2 0.5s ease 0s infinite alternate;
animation: animation1shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, 16px);
transform: translate(-16px, 16px);
}
}
@keyframes animation1shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, 16px);
transform: translate(-16px, 16px);
}
}
.loading1 .shape3 {
-webkit-animation: animation1shape3 0.5s ease 0s infinite alternate;
animation: animation1shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, -16px);
transform: translate(16px, -16px);
}
}
@keyframes animation1shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, -16px);
transform: translate(16px, -16px);
}
}
.loading1 .shape4 {
-webkit-animation: animation1shape4 0.5s ease 0s infinite alternate;
animation: animation1shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, -16px);
transform: translate(-16px, -16px);
}
}
@keyframes animation1shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, -16px);
transform: translate(-16px, -16px);
}
}
</style>

View File

@@ -0,0 +1,176 @@
<template>
<view class="container loading2">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading2',
data() {
return {};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading2 {
-webkit-transform: rotate(10deg);
transform: rotate(10deg);
}
.container.loading2 .shape {
border-radius: 5px;
}
.container.loading2 {
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading2 .shape1 {
-webkit-animation: animation2shape1 0.5s ease 0s infinite alternate;
animation: animation2shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, 20px);
transform: translate(20px, 20px);
}
}
@keyframes animation2shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, 20px);
transform: translate(20px, 20px);
}
}
.loading2 .shape2 {
-webkit-animation: animation2shape2 0.5s ease 0s infinite alternate;
animation: animation2shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, 20px);
transform: translate(-20px, 20px);
}
}
@keyframes animation2shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, 20px);
transform: translate(-20px, 20px);
}
}
.loading2 .shape3 {
-webkit-animation: animation2shape3 0.5s ease 0s infinite alternate;
animation: animation2shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, -20px);
transform: translate(20px, -20px);
}
}
@keyframes animation2shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, -20px);
transform: translate(20px, -20px);
}
}
.loading2 .shape4 {
-webkit-animation: animation2shape4 0.5s ease 0s infinite alternate;
animation: animation2shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, -20px);
transform: translate(-20px, -20px);
}
}
@keyframes animation2shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, -20px);
transform: translate(-20px, -20px);
}
}
</style>

View File

@@ -0,0 +1,182 @@
<template>
<view class="container loading3">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading3',
data() {
return {};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading3 {
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading3 .shape1 {
border-top-left-radius: 10px;
}
.container.loading3 .shape2 {
border-top-right-radius: 10px;
}
.container.loading3 .shape3 {
border-bottom-left-radius: 10px;
}
.container.loading3 .shape4 {
border-bottom-right-radius: 10px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading3 .shape1 {
-webkit-animation: animation3shape1 0.5s ease 0s infinite alternate;
animation: animation3shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, 5px);
transform: translate(5px, 5px);
}
}
@keyframes animation3shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, 5px);
transform: translate(5px, 5px);
}
}
.loading3 .shape2 {
-webkit-animation: animation3shape2 0.5s ease 0s infinite alternate;
animation: animation3shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, 5px);
transform: translate(-5px, 5px);
}
}
@keyframes animation3shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, 5px);
transform: translate(-5px, 5px);
}
}
.loading3 .shape3 {
-webkit-animation: animation3shape3 0.5s ease 0s infinite alternate;
animation: animation3shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, -5px);
transform: translate(5px, -5px);
}
}
@keyframes animation3shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, -5px);
transform: translate(5px, -5px);
}
}
.loading3 .shape4 {
-webkit-animation: animation3shape4 0.5s ease 0s infinite alternate;
animation: animation3shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, -5px);
transform: translate(-5px, -5px);
}
}
@keyframes animation3shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, -5px);
transform: translate(-5px, -5px);
}
}
</style>

View File

@@ -0,0 +1,227 @@
<template>
<view class="container loading5">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading5',
data() {
return {};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading5 .shape {
width: 15px;
height: 15px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading5 .shape1 {
animation: animation5shape1 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
50% {
-webkit-transform: translate(15px, 15px);
transform: translate(15px, 15px);
}
75% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
}
@keyframes animation5shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
50% {
-webkit-transform: translate(15px, 15px);
transform: translate(15px, 15px);
}
75% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
}
.loading5 .shape2 {
animation: animation5shape2 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
50% {
-webkit-transform: translate(-15px, 15px);
transform: translate(-15px, 15px);
}
75% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
}
@keyframes animation5shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
50% {
-webkit-transform: translate(-15px, 15px);
transform: translate(-15px, 15px);
}
75% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
}
.loading5 .shape3 {
animation: animation5shape3 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
50% {
-webkit-transform: translate(15px, -15px);
transform: translate(15px, -15px);
}
75% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
}
@keyframes animation5shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
50% {
-webkit-transform: translate(15px, -15px);
transform: translate(15px, -15px);
}
75% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
}
.loading5 .shape4 {
animation: animation5shape4 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
50% {
-webkit-transform: translate(-15px, -15px);
transform: translate(-15px, -15px);
}
75% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
}
@keyframes animation5shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
50% {
-webkit-transform: translate(-15px, -15px);
transform: translate(-15px, -15px);
}
75% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
}
</style>

View File

@@ -0,0 +1,235 @@
<template>
<view class="container loading6">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading6',
data() {
return {};
}
}
</script>
<style scoped="true">
.container {
width: 30px;
height: 30px;
position: relative;
}
.container.loading6 {
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading6 .shape {
width: 12px;
height: 12px;
border-radius: 2px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890FF;
}
.container .shape.shape2 {
right: 0;
background-color: #91CB74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #FAC858;
}
.container .shape.shape4 {
bottom: 0;
right: 0;
background-color: #EE6666;
}
.loading6 .shape1 {
-webkit-animation: animation6shape1 2s linear 0s infinite normal;
animation: animation6shape1 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
50% {
-webkit-transform: translate(18px, 18px);
transform: translate(18px, 18px);
}
75% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
}
@keyframes animation6shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
50% {
-webkit-transform: translate(18px, 18px);
transform: translate(18px, 18px);
}
75% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
}
.loading6 .shape2 {
-webkit-animation: animation6shape2 2s linear 0s infinite normal;
animation: animation6shape2 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
50% {
-webkit-transform: translate(-18px, 18px);
transform: translate(-18px, 18px);
}
75% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
}
@keyframes animation6shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
50% {
-webkit-transform: translate(-18px, 18px);
transform: translate(-18px, 18px);
}
75% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
}
.loading6 .shape3 {
-webkit-animation: animation6shape3 2s linear 0s infinite normal;
animation: animation6shape3 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
50% {
-webkit-transform: translate(18px, -18px);
transform: translate(18px, -18px);
}
75% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
}
@keyframes animation6shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
50% {
-webkit-transform: translate(18px, -18px);
transform: translate(18px, -18px);
}
75% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
}
.loading6 .shape4 {
-webkit-animation: animation6shape4 2s linear 0s infinite normal;
animation: animation6shape4 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
50% {
-webkit-transform: translate(-18px, -18px);
transform: translate(-18px, -18px);
}
75% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
}
@keyframes animation6shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
50% {
-webkit-transform: translate(-18px, -18px);
transform: translate(-18px, -18px);
}
75% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<view>
<Loading1 v-if="loadingType==1" />
<Loading2 v-if="loadingType==2" />
<Loading3 v-if="loadingType==3" />
<Loading4 v-if="loadingType==4" />
<Loading5 v-if="loadingType==5" />
</view>
</template>
<script>
import Loading1 from "./loading1.vue";
import Loading2 from "./loading2.vue";
import Loading3 from "./loading3.vue";
import Loading4 from "./loading4.vue";
import Loading5 from "./loading5.vue";
export default {
components: {Loading1, Loading2, Loading3, Loading4, Loading5},
name: 'qiun-loading',
props: {
loadingType: {
type: Number,
default: 2
},
},
data() {
return {};
},
}
</script>
<style>
</style>

View File

@@ -0,0 +1,455 @@
<template>
<view class="uni-select-lay" :style="{ 'z-index': zindex }">
<input type="text" :name="name" v-model="value" class="uni-select-input" readonly />
<view class="uni-select-lay-select" :class="{ active: active }">
<!-- 禁用mask -->
<view class="uni-disabled" v-if="disabled"></view>
<!-- 禁用mask -->
<!-- 清空 -->
<view class="uni-select-lay-input-close" v-if="changevalue != '' && this.active"><text @click.stop="removevalue"></text></view>
<!-- 清空 -->
<input
type="text"
readonly
disabled="true"
class="uni-select-lay-input"
:class="{ active: changevalue != '' && changevalue != placeholder }"
v-model="changevalue"
:placeholder="placeholder"
@focus="unifocus"
@input="intchange"
@blur="uniblur"
@click.stop="select"
/>
<view class="uni-select-lay-icon" :class="{ disabled: disabled }" @click.stop="select"><text></text></view>
</view>
<view class="uni-date-mask" v-show="active" @click.stop="select"></view>
<scroll-view class="uni-select-lay-options" :scroll-y="true" v-show="active" @scroll="selectmove" @touchstart="movetouch">
<template v-if="!changes">
<view class="uni-select-lay-item" v-if="showplaceholder" :class="{ active: value == '' }" @click.stop="selectitem(-1, null)">{{ placeholder }}</view>
<view class="uni-select-lay-item" :class="{ active: value == item[svalue], disabled: item.disabled }" v-for="(item, index) in options" :key="index" @click.stop="selectitem(index, item)">
{{ item[slabel] }}
</view>
</template>
<!-- 搜索 -->
<template v-else>
<template v-if="vlist.length > 0">
<view class="uni-select-lay-item" :class="{ active: value == item[svalue] }" v-for="(item, index) in vlist" :key="index" @click.stop="selectitem(index, item)">
{{ item[slabel] }}
</view>
</template>
<template v-else>
<view class="nosearch">{{ changesValue }}</view>
</template>
</template>
</scroll-view>
</view>
</template>
<script>
export default {
name: 'select-lay',
props: {
disabled: {
type: Boolean,
default: false
},
zindex: {
type: Number,
default: 999
},
options: {
type: Array,
default() {
return [];
}
},
name: {
type: String,
default: ''
},
value: {
type: [String,Number],
default: ''
},
placeholder: {
type: String,
default: '请选择'
},
showplaceholder: {
type: Boolean,
default: true
},
slabel: {
type: String,
default: 'label'
},
svalue: {
type: String,
default: 'value'
}
},
data() {
return {
active: false, //组件是否激活,
isfocus: false, //是否有焦点
isremove: false, //是否是因为点击清空才导致的失去焦点
ismove: false, //是否是因为移动才失去焦点
changevalue: '', //搜索框同步
oldvalue: '', //数据回滚
changes: false, //正在搜索
changesValue: '',
vlist: [], //搜索框查询的列表
settimer: null //value改变定时器
};
},
mounted() {
this.itemcheck();
},
watch: {
//value改变
value() {
this.itemcheck();
},
//初始化数组
options() {
// 此处判断是否有初始value,存在则判断显示文字
this.itemcheck();
}
},
methods: {
//判断数组跟当前active值
itemcheck() {
// 此处判断是否有初始value,存在则判断显示文字
if (this.value != '') {
// 展示plachhoder
//判断数组
if (this.options.length > 0) {
this.options.forEach(item => {
if (this.value == item[this.svalue]) {
this.oldvalue = this.changevalue = item[this.slabel];
return;
}
});
}
} else {
this.oldvalue = this.changevalue = '';
}
},
//点击组件
select() {
if (this.disabled) return;
this.active = !this.active;
if (this.active) {
this.changes = false;
} else {
this.changevalue = this.oldvalue;
}
},
// 获得焦点
unifocus() {
if (this.disabled) return;
this.active = true;
this.changes = false;
this.isfocus = true;
},
// 失去焦点
uniblur() {
this.isfocus = false;
// bug 点击组件列会先触发失去焦点,此时组件列事件不执行
setTimeout(() => {
if (this.isremove || this.ismove) {
this.isremove = false;
this.ismove = false;
} else {
this.changevalue = this.oldvalue;
this.isremove = false;
this.active = false;
}
}, 153);
},
movetouch() {
setTimeout(() => {
if (this.isfocus) {
this.ismove = false;
return;
}
if (!this.ismove) this.ismove = true;
}, 100);
// this.changes = false;
},
selectmove() {
setTimeout(() => {
if (this.isfocus) {
this.ismove = false;
return;
}
if (!this.ismove) this.ismove = true;
}, 100);
// this.changes = false;
},
//移除数据
removevalue() {
this.isremove = true;
this.changes = false;
this.changevalue = '';
},
//value 改变
intchange() {
if (this.changevalue == '') {
this.changes = false;
return;
}
if (this.oldvalue == this.changevalue) {
return;
}
this.vlist = [];
this.changes = true;
this.changesValue = '正在搜索...';
if (this.settimer) {
clearTimeout(this.settimer);
}
this.settimer = setTimeout(() => {
this.vlist = this.options.filter(item => {
return item[this.slabel].includes(this.changevalue);
});
if (this.vlist.length === 0) {
this.changesValue = '暂无匹配内容!';
}
}, 600);
},
//点击组件列
selectitem(index, item) {
if (item && item.disabled) {
return false;
}
this.changevalue = this.oldvalue;
this.active = false;
this.$emit('selectitem', index, item);
}
}
};
</script>
<style lang="scss" scoped>
.uni-select-lay {
position: relative;
z-index: 999;
box-sizing: border-box;
.uni-select-input {
opacity: 0;
position: absolute;
z-index: -111;
}
// select部分
.uni-select-lay-select {
user-select: none;
position: relative;
z-index: 3;
height: 0.32rem;
padding: 0 0.3rem 0 0.1rem;
box-sizing: border-box;
border-radius: 0.02rem;
border: 0.01rem solid rgb(229, 229, 229);
display: flex;
align-items: center;
font-size: 0.14rem;
color: #999;
.uni-disabled {
position: absolute;
left: 0;
width: 100%;
height: 100%;
z-index: 19;
cursor: no-drop;
background: rgba(255, 255, 255, 0.5);
}
// input 框的清除按钮
.uni-select-lay-input-close {
position: absolute;
right: 0.35rem;
top: 0;
height: 100%;
width: 0.15rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
cursor: pointer;
text {
position: relative;
background: #fff;
width: 0.13rem;
height: 0.13rem;
border-radius: 50%;
border: 0.01rem solid #bbb;
&::before,
&::after {
content: '';
position: absolute;
left: 20%;
top: 50%;
height: 0.01rem;
width: 60%;
transform: rotate(45deg);
background-color: #bbb;
}
&::after {
transform: rotate(-45deg);
}
}
}
.uni-select-lay-input {
font-size: 0.14rem;
color: #999;
display: block;
width: 98%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 0.3rem;
box-sizing: border-box;
&.active {
color: #333;
}
}
.uni-select-lay-icon {
cursor: pointer;
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 0.3rem;
display: flex;
align-items: center;
justify-content: center;
&::before {
content: '';
width: 0.01rem;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: #e5e5e5;
}
text {
display: block;
width: 0;
height: 0;
border-width: 0.07rem 0.07rem 0;
border-style: solid;
border-color: #bbb transparent transparent;
transition: 0.3s;
}
&.disabled {
cursor: no-drop;
text {
width: 0.2rem;
height: 0.2rem;
border: 0.02rem solid #ff0000;
border-radius: 50%;
transition: 0.3s;
position: relative;
z-index: 999;
&::after {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 0.02rem;
margin-top: -0.01rem;
background-color: #ff0000;
transform: rotate(45deg);
}
}
}
}
&.active .uni-select-lay-icon {
text {
transform: rotate(180deg);
}
}
}
// options部分
.uni-select-lay-options {
user-select: none;
position: absolute;
top: calc(100% + 0.05rem);
left: 0;
width: 100%;
// height: 500rpx;
max-height: 2.5rem;
// overflow-y: auto;
border-radius: 0.02rem;
border: 1px solid rgb(229, 229, 229);
background: #fff;
padding: 0.05rem 0;
box-sizing: border-box;
z-index: 9;
.uni-select-lay-item {
padding: 0 0.1rem;
box-sizing: border-box;
cursor: pointer;
line-height: 2.5;
transition: 0.3s;
font-size: 0.14rem;
&.active {
background: $primary-color;
color: #fff;
&:hover {
background: $primary-color;
color: #fff;
}
}
&.disabled {
color: #999;
cursor: not-allowed;
}
&:hover {
background-color: #f5f5f5;
}
}
.nosearch {
font-size: 0.16rem;
line-height: 3;
text-align: center;
color: #666;
}
}
}
.uni-date-mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0);
transition-duration: 0.3s;
z-index: 8;
}
</style>

View File

@@ -0,0 +1,480 @@
<template>
<unipopup ref="dialogRef" type="center" :maskClick="false">
<view class="stock-dialog-wrap">
<view class="stock-dialog-head">
<text>商品选择</text>
<text class="iconfont iconguanbi1" @click="$emit('change', false)"></text>
</view>
<view class="stock-dialog-body">
<view class="tree">
<scroll-view scroll-y="true" class="list-wrap">
<view class="item" :class="{ 'active': option.category_id === '' }" @click="itemClick({ category_id: '', child_num: 0 })">
<view class="icon"></view>
<view>全部分类</view>
</view>
<view v-for="(item, key) in goodsCategoryList" :key="key">
<view class="item" :class="{ 'active': option.category_id === item.category_id }" @click="itemClick(item)">
<view class="icon" :class="{ 'active': activeList.indexOf(item.category_id) != -1 }">
<text v-if="item.child_num" class="iconfont iconsanjiao_xia"></text>
</view>
<view>{{ item.title }}</view>
</view>
<template v-if="item.child_num">
<view v-show="activeList.indexOf(item.category_id) != -1" v-for="(item2, key2) in item.children" :key="key2" class="level">
<view class="item" :class="{ 'active': option.category_id === item2.category_id }" @click="itemClick(item2)">
<view class="icon" :class="{ 'active': activeList.indexOf(item2.category_id) != -1 }">
<text v-if="item2.child_num" class="iconfont iconsanjiao_xia"></text>
</view>
<view>{{ item2.title }}</view>
</view>
<template>
<view v-show="activeList.indexOf(item2.category_id) != -1" v-for="(item3, key3) in item2.children" :key="key3" class="level">
<view class="item item2" @click="itemClick(item3)">
<view class="icon"></view>
<view>{{ item3.title }}</view>
</view>
</view>
</template>
</view>
</template>
</view>
</scroll-view>
</view>
<view class="stock-dialog-table">
<view class="search common-form">
<view class="common-form-item">
<view class="form-inline">
<view class="form-input-inline">
<input type="text" v-model="option.search_text" @confirm="getStoreGoods" placeholder="请输入产品名称/规格/编码" class="form-input" />
</view>
</view>
<view class="form-inline common-btn-wrap">
<button type="default" class="screen-btn" @click="getStoreGoods">筛选</button>
</view>
</view>
</view>
<uniDataTable :url="url" :option="option" :cols="cols" :pagesize="8" ref="goodsListTable" @checkBox="checkBox" @tableData="tableDataChange"></uniDataTable>
</view>
</view>
<view class="btn">
<button type="primary" class="default-btn submit" @click="submit('close')">选中</button>
<button type="primary" class="default-btn" @click="$emit('change', false)">取消</button>
</view>
</view>
</unipopup>
</template>
<script>
import unipopup from '@/components/uni-popup/uni-popup.vue';
import uniDataTable from '@/components/uni-data-table/uni-data-table.vue';
import {getManageGoodsCategory} from '@/api/goods.js';
export default {
name: 'stockDialog',
components: {
unipopup,
uniDataTable
},
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: Boolean,
default: false
},
params: {
type: Object,
default: ()=>{
return {}
}
},
apiType: {
type: String,
default: 'sku' //选择是sku 还是 spu
}
},
data() {
return {
goodsCategoryList: {},
activeList: [],//下拉激活
option: {
category_id: '',
search_text: '',
is_weigh: 0,
page_size: 8,
},
cols: [{
width: 6,
align: 'center',
checkbox: true
}, {
field: 'account_data',
width: 50,
title: '商品信息',
align: 'left',
templet: data => {
let img = this.$util.img(data.sku_image);
let html = `
<view class="goods-content">
<image class="goods-img" src="${img}" mode="aspectFit"/>
<text class="goods-name multi-hidden" title="${data.sku_name}">${data.sku_name}</text>
</view>
`;
return html;
}
}, {
field: 'real_stock',
width: 22,
title: '库存',
align: 'center',
templet: data => {
return (data.real_stock || 0);
}
}, {
width: 22,
title: '单位',
templet: data => {
return (data.unit || '件');
}
}],
checkList: {},
url: '/stock/storeapi/manage/getStoreGoods'
}
},
watch: {
value: {
handler: function (val) {
if (val) {
this.$nextTick(() => {
this.option = Object.assign(this.option, this.params);
if (this.params.temp_store_id && this.params.temp_store_id == '') {
delete this.option.temp_store_id
}
this.$refs.dialogRef.open()
})
} else {
this.$nextTick(() => {
this.option = Object(this.option, {
category_id: '',
search_text: '',
is_weigh: 0,
page: 1,
page_size: 8,
});
this.checkList = {};
this.$refs.dialogRef.close()
})
}
},
immediate: true
},
apiType: {
handler: function (val) {
if(val == 'sku'){
this.cols = [{
width: 6,
align: 'center',
checkbox: true
}, {
field: 'account_data',
width: 50,
title: '商品信息',
align: 'left',
templet: data => {
let img = this.$util.img(data.sku_image);
let html = `
<view class="goods-content">
<image class="goods-img" src="${img}" mode="aspectFit"/>
<text class="goods-name multi-hidden" title="${data.sku_name}">${data.sku_name}</text>
</view>
`;
return html;
}
}, {
field: 'real_stock',
width: 22,
title: '库存',
align: 'center',
templet: data => {
return (data.real_stock || 0);
}
}, {
width: 22,
title: '单位',
templet: data => {
return (data.unit || '件');
}
}];
this.url = '/stock/storeapi/manage/getStoreGoods';
}else if(val == 'spu'){
this.cols = [{
width: 6,
align: 'center',
checkbox: true
}, {
field: 'account_data',
width: 50,
title: '商品信息',
align: 'left',
templet: data => {
let img = this.$util.img(data.goods_image);
let html = `
<view class="goods-content">
<image class="goods-img" src="${img}" mode="aspectFit"/>
<text class="goods-name multi-hidden" title="${data.goods_name}">${data.goods_name}</text>
</view>
`;
return html;
}
}, {
field: 'goods_stock',
width: 22,
title: '库存',
align: 'center',
templet: data => {
return (data.goods_stock || 0);
}
}, {
width: 22,
title: '商品类型',
templet: data => {
return (data.goods_class_name || '--');
}
}];
this.url = '/cashier/storeapi/goods/getGoodsListBySelect';
}
},
immediate: true
}
},
mounted() {
this.getGoodsCategory()
},
methods: {
getGoodsCategory() {
getManageGoodsCategory().then(res=>{
uni.hideLoading();
if (res.data && Object.keys(res.data)) {
this.goodsCategoryList = res.data
} else {
this.$util.showToast({
title: res.message
});
}
})
},
itemClick(item) {//tree点击
this.option.category_id = item.category_id;
var index = this.activeList.indexOf(item.category_id);
if (item.child_num && index === -1) {
this.activeList.push(item.category_id);
} else if (item.child_num && index != -1) {
this.activeList.splice(index, 1);
}
this.$forceUpdate();
this.getStoreGoods();
},
getStoreGoods() {//表格查询
this.$refs.goodsListTable.load({
page: 1
});
},
checkBox(list, listIndex) {
this.checkList[this.$refs.goodsListTable.page] = {};
this.checkList[this.$refs.goodsListTable.page].data = list;
this.checkList[this.$refs.goodsListTable.page].index = listIndex;
},
tableDataChange(){
if(this.checkList[this.$refs.goodsListTable.page])
this.$refs.goodsListTable.defaultSelectData(this.checkList[this.$refs.goodsListTable.page].data, this.checkList[this.$refs.goodsListTable.page].index);
},
submit(val) {
if (!Object.values(this.checkList).length) {
this.$util.showToast({
title: '请选择商品'
});
return false
}
let data = [];
Object.values(this.checkList).forEach((item,index)=>{
data = data.concat(item.data)
});
this.$emit('selectGoods', data);
if(val !='submit'){
this.$emit('change', false);
}else{
this.$refs.goodsListTable.clearCheck();
}
this.checkList = [];
}
}
}
</script>
<style lang="scss" scoped>
.stock-dialog-wrap {
background-color: #fff;
border-radius: 0.05rem;
width: 9rem;
.stock-dialog-head {
padding: 0 0.15rem;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.15rem;
height: 0.45rem;
border-bottom: 0.01rem solid #e8eaec;
.iconguanbi1 {
font-size: $uni-font-size-lg;
}
}
.stock-dialog-body {
width: 100%;
height: 7.3rem;
padding: 0.1rem 0.2rem 0 0.2rem;
box-sizing: border-box;
display: flex;
.tree {
width: 1.8rem;
height: 7.1rem;
overflow-y: auto;
border-right: 0.01rem solid #e8eaec;
flex-shrink: 0;
flex-basis: auto;
flex-grow: 0;
box-sizing: border-box;
.list-wrap {
width: 100%;
height: 100%;
>view {
box-sizing: border-box;
width: 100%;
}
view.item {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
line-height: 0.3rem;
min-height: 0.3rem;
font-weight: 500;
&.active {
.icon,
view {
color: $primary-color !important;
}
background-color: #f7f7f7;
}
&:hover {
background-color: #f7f7f7;
}
.icon {
width: 0.2rem;
height: 0.3rem;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(-90deg);
transition: all ease 0.5s;
&.active {
transform: rotate(-45deg);
}
}
}
.level {
width: 100%;
box-sizing: border-box;
.item {
padding-left: 0.2rem;
}
.item2 {
padding-left: 0.4rem;
}
}
}
}
.stock-dialog-table {
width: 6.6rem;
margin-left: 0.2rem;
.search {
display: flex;
justify-content: flex-end;
}
}
}
.btn {
display: flex;
justify-content: flex-end;
border-top: 0.01rem solid #e8eaec;
padding: 0.1rem 0.2rem 0.1rem 0.2rem;
height: 0.38rem;
.default-btn,
.primary-btn {
margin: 0;
}
.default-btn {
border: 0.01rem solid #e8eaec !important;
}
.submit{
margin-right: 0.15rem;
}
.default-btn::after {
display: none;
}
}
.common-form .common-btn-wrap {
margin-left: 0;
}
.common-form .common-btn-wrap .screen-btn {
margin-right: 0;
}
.common-form .common-form-item {
margin-bottom: 0.1rem;
}
/deep/ .goods-content {
display: flex;
.goods-img {
margin-right: 0.1rem;
width: 0.5rem;
height: 0.5rem;
flex-shrink: 0;
flex-basis: auto;
flex-grow: 0;
}
}
}
</style>

View File

@@ -0,0 +1,420 @@
/*
* uCharts®
* 高性能跨平台图表库支持H5、APP、小程序微信/支付宝/百度/头条/QQ/360、Vue、Taro等支持canvas的框架平台
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释,感谢支持开源!
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
// 通用配置项
// 主题颜色配置如每个图表类型需要不同主题请在对应图表类型上更改color属性
const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
module.exports = {
//demotype为自定义图表类型
"type": ["pie", "ring", "rose", "funnel", "line", "column", "area", "radar", "gauge","candle","demotype"],
//增加自定义图表类型如果需要categories请在这里加入您的图表类型例如最后的"demotype"
"categories": ["line", "column", "area", "radar", "gauge", "candle","demotype"],
//instance为实例变量承载属性option为eopts承载属性不要删除
"instance": {},
"option": {},
//下面是自定义format配置因除H5端外的其他端无法通过props传递函数只能通过此属性对应下标的方式来替换
"formatter":{
"tooltipDemo1":function(res){
let result = ''
for (let i in res) {
if (i == 0) {
result += res[i].axisValueLabel + '年销售额'
}
let value = '--'
if (res[i].data !== null) {
value = res[i].data
}
// #ifdef H5
result += '\n' + res[i].seriesName + '' + value + ' 万元'
// #endif
// #ifdef APP-PLUS
result += '<br/>' + res[i].marker + res[i].seriesName + '' + value + ' 万元'
// #endif
}
return result;
},
legendFormat:function(name){
return "自定义图例+"+name;
},
yAxisFormatDemo:function (value, index) {
return value + '元';
},
seriesFormatDemo:function(res){
return res.name + '年' + res.value + '元';
}
},
//这里演示了自定义您的图表类型的option可以随意命名之后在组件上 type="demotype" 后组件会调用这个花括号里的option如果组件上还存在eopts参数会将demotype与eopts中option合并后渲染图表。
"demotype":{
"color": color,
//在这里填写echarts的option即可
},
//下面是自定义配置,请添加项目所需的通用配置
"column": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'axis'
},
"grid": {
"top": 30,
"bottom": 50,
"right": 15,
"left": 40
},
"legend": {
"bottom": 'left',
},
"toolbox": {
"show": false,
},
"xAxis": {
"type": 'category',
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
"boundaryGap": true,
"data": []
},
"yAxis": {
"type": 'value',
"axisTick": {
"show": false,
},
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
},
"seriesTemplate": {
"name": '',
"type": 'bar',
"data": [],
"barwidth": 20,
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
},
},
"line": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'axis'
},
"grid": {
"top": 30,
"bottom": 50,
"right": 15,
"left": 40
},
"legend": {
"bottom": 'left',
},
"toolbox": {
"show": false,
},
"xAxis": {
"type": 'category',
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
"boundaryGap": true,
"data": []
},
"yAxis": {
"type": 'value',
"axisTick": {
"show": false,
},
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
},
"seriesTemplate": {
"name": '',
"type": 'line',
"data": [],
"barwidth": 20,
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
},
},
"area": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'axis'
},
"grid": {
"top": 30,
"bottom": 50,
"right": 15,
"left": 40
},
"legend": {
"bottom": 'left',
},
"toolbox": {
"show": false,
},
"xAxis": {
"type": 'category',
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
"boundaryGap": true,
"data": []
},
"yAxis": {
"type": 'value',
"axisTick": {
"show": false,
},
"axisLabel": {
"color": '#666666'
},
"axisLine": {
"lineStyle": {
"color": '#CCCCCC'
}
},
},
"seriesTemplate": {
"name": '',
"type": 'line',
"data": [],
"areaStyle": {},
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
},
},
"pie": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'item'
},
"grid": {
"top": 40,
"bottom": 30,
"right": 15,
"left": 15
},
"legend": {
"bottom": 'left',
},
"seriesTemplate": {
"name": '',
"type": 'pie',
"data": [],
"radius": '50%',
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
},
},
"ring": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'item'
},
"grid": {
"top": 40,
"bottom": 30,
"right": 15,
"left": 15
},
"legend": {
"bottom": 'left',
},
"seriesTemplate": {
"name": '',
"type": 'pie',
"data": [],
"radius": ['40%', '70%'],
"avoidLabelOverlap": false,
"label": {
"show": true,
"color": "#666666",
"position": 'top',
},
"labelLine": {
"show": true
},
},
},
"rose": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'item'
},
"legend": {
"top": 'bottom'
},
"seriesTemplate": {
"name": '',
"type": 'pie',
"data": [],
"radius": "55%",
"center": ['50%', '50%'],
"rosetype": 'area',
},
},
"funnel": {
"color": color,
"title": {
"text": ''
},
"tooltip": {
"trigger": 'item',
"formatter": "{b} : {c}%"
},
"legend": {
"top": 'bottom'
},
"seriesTemplate": {
"name": '',
"type": 'funnel',
"left": '10%',
"top": 60,
"bottom": 60,
"width": '80%',
"min": 0,
"max": 100,
"minSize": '0%',
"maxSize": '100%',
"sort": 'descending',
"gap": 2,
"label": {
"show": true,
"position": 'inside'
},
"labelLine": {
"length": 10,
"lineStyle": {
"width": 1,
"type": 'solid'
}
},
"itemStyle": {
"bordercolor": '#fff',
"borderwidth": 1
},
"emphasis": {
"label": {
"fontSize": 20
}
},
"data": [],
},
},
"gauge": {
"color": color,
"tooltip": {
"formatter": '{a} <br/>{b} : {c}%'
},
"seriesTemplate": {
"name": '业务指标',
"type": 'gauge',
"detail": {"formatter": '{value}%'},
"data": [{"value": 50, "name": '完成率'}]
},
},
"candle": {
"xAxis": {
"data": []
},
"yAxis": {},
"color": color,
"title": {
"text": ''
},
"dataZoom": [{
"type": 'inside',
"xAxisIndex": [0, 1],
"start": 10,
"end": 100
},
{
"show": true,
"xAxisIndex": [0, 1],
"type": 'slider',
"bottom": 10,
"start": 10,
"end": 100
}
],
"seriesTemplate": {
"name": '',
"type": 'k',
"data": [],
},
}
}

View File

@@ -0,0 +1,433 @@
/*
* uCharts®
* 高性能跨平台图表库支持H5、APP、小程序微信/支付宝/百度/头条/QQ/360、Vue、Taro等支持canvas的框架平台
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释,感谢支持开源!
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
// 主题颜色配置如每个图表类型需要不同主题请在对应图表类型上更改color属性
const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
module.exports = {
//demotype为自定义图表类型
"type":["pie","ring","rose","word","funnel","map","arcbar","line","column","area","radar","gauge","candle","mix","demotype"],
"range":["饼状图","圆环图","玫瑰图","词云图","漏斗图","地图","圆弧进度条","折线图","柱状图","区域图","雷达图","仪表盘","K线图","混合图","自定义类型"],
//增加自定义图表类型如果需要categories请在这里加入您的图表类型例如最后的"demotype"
"categories":["line","column","area","radar","gauge","candle","mix","demotype"],
//instance为实例变量承载属性option为eopts承载属性不要删除
"instance":{},
"option":{},
//下面是自定义format配置因除H5端外的其他端无法通过props传递函数只能通过此属性对应下标的方式来替换
"formatter":{
"yAxisDemo1":function(val){return val+'元'},
"yAxisDemo2":function(val){return val.toFixed(2)},
"seriesDemo1":function(val){
return val+'元'
},
"tooltipDemo1":function(item, category, index, opts){
if(index==0){
return '随便用'+item.data+'年'
}else{
return '其他我没改'+item.data+'天'
}
},
"pieDemo":function(val, index, series){
if(index !== undefined){
return series[index].name+''+series[index].data+'元'
}
},
},
//这里演示了自定义您的图表类型的option可以随意命名之后在组件上 type="demotype" 后组件会调用这个花括号里的option如果组件上还存在opts参数会将demotype与opts中option合并后渲染图表。
"demotype":{
//我这里把曲线图当做了自定义图表类型,您可以根据需要随意指定类型或配置
"type": "line",
"color": color,
"padding": [15,10,0,15],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"gridType": "dash",
"dashLength": 2,
},
"legend": {
},
"extra": {
"line": {
"type": "curve",
"width": 2
},
}
},
//下面是自定义配置,请添加项目所需的通用配置
"pie":{
"type": "pie",
"color": color,
"padding": [5,5,5,5],
"extra": {
"pie": {
"activeOpacity": 0.5,
"activeRadius": 10,
"offsetAngle": 0,
"labelWidth": 15,
"border": true,
"borderWidth": 3,
"borderColor": "#FFFFFF"
},
}
},
"ring":{
"type": "ring",
"color": color,
"padding": [5,5,5,5],
"rotate": false,
"dataLabel": true,
"legend": {
"show": true,
"position": "right",
"lineHeight": 25,
},
"title": {
"name": "收益率",
"fontSize": 15,
"color": "#666666"
},
"subtitle": {
"name": "70%",
"fontSize": 25,
"color": "#7cb5ec"
},
"extra": {
"ring": {
"ringWidth":30,
"activeOpacity": 0.5,
"activeRadius": 10,
"offsetAngle": 0,
"labelWidth": 15,
"border": true,
"borderWidth": 3,
"borderColor": "#FFFFFF"
},
},
},
"rose":{
"type": "rose",
"color": color,
"padding": [5,5,5,5],
"legend": {
"show": true,
"position": "left",
"lineHeight": 25,
},
"extra": {
"rose": {
"type": "area",
"minRadius": 50,
"activeOpacity": 0.5,
"activeRadius": 10,
"offsetAngle": 0,
"labelWidth": 15,
"border": false,
"borderWidth": 2,
"borderColor": "#FFFFFF"
},
}
},
"word":{
"type": "word",
"color": color,
"extra": {
"word": {
"type": "normal",
"autoColors": false
}
}
},
"funnel":{
"type": "funnel",
"color": color,
"padding": [15,15,0,15],
"extra": {
"funnel": {
"activeOpacity": 0.3,
"activeWidth": 10,
"border": true,
"borderWidth": 2,
"borderColor": "#FFFFFF",
"fillOpacity": 1,
"labelAlign": "right"
},
}
},
"map":{
"type": "map",
"color": color,
"padding": [0,0,0,0],
"dataLabel": true,
"extra": {
"map": {
"border": true,
"borderWidth": 1,
"borderColor": "#666666",
"fillOpacity": 0.6,
"activeBorderColor": "#F04864",
"activeFillColor": "#FACC14",
"activeFillOpacity": 1
},
}
},
"arcbar":{
"type": "arcbar",
"color": color,
"title": {
"name": "百分比",
"fontSize": 25,
"color": "#00FF00"
},
"subtitle": {
"name": "默认标题",
"fontSize": 15,
"color": "#666666"
},
"extra": {
"arcbar": {
"type": "default",
"width": 12,
"backgroundColor": "#E9E9E9",
"startAngle": 0.75,
"endAngle": 0.25,
"gap": 2
}
}
},
"line":{
"type": "line",
"color": color,
"padding": [15,10,0,15],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"gridType": "dash",
"dashLength": 2,
},
"legend": {
},
"extra": {
"line": {
"type": "straight",
"width": 2
},
}
},
"column":{
"type": "column",
"color": color,
"padding": [15,15,0,5],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
},
"legend": {
},
"extra": {
"column": {
"type": "group",
"width": 30,
"meterBorde": 1,
"meterFillColor": "#FFFFFF",
"activeBgColor": "#000000",
"activeBgOpacity": 0.08
},
}
},
"area":{
"type": "area",
"color": color,
"padding": [15,15,0,15],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"gridType": "dash",
"dashLength": 2,
},
"legend": {
},
"extra": {
"area": {
"type": "straight",
"opacity": 0.2,
"addLine": true,
"width": 2,
"gradient": false
},
}
},
"radar":{
"type": "radar",
"color": color,
"padding": [5,5,5,5],
"legend": {
"show": true,
"position": "right",
"lineHeight": 25,
},
"extra": {
"radar": {
"gridType": "radar",
"gridColor": "#CCCCCC",
"gridCount": 3,
"opacity": 0.2,
"labelColor": "#666666",
"max": 200
},
}
},
"gauge":{
"type": "gauge",
"color": color,
"title": {
"name": "66Km/H",
"fontSize": 25,
"color": "#2fc25b",
"offsetY": 50
},
"subtitle": {
"name": "实时速度",
"fontSize": 15,
"color": "#1890ff",
"offsetY": -50
},
"extra": {
"gauge": {
"type": "default",
"width": 30,
"labelColor": "#666666",
"startAngle": 0.75,
"endAngle": 0.25,
"startNumber": 0,
"endNumber": 100,
"labelFormat": "",
"splitLine": {
"fixRadius": 0,
"splitNumber": 10,
"width": 30,
"color": "#FFFFFF",
"childNumber": 5,
"childWidth": 12
},
"pointer": {
"width": 24,
"color": "auto"
}
}
}
},
"candle":{
"type": "candle",
"color": color,
"padding": [15,15,0,15],
"enableScroll": true,
"enableMarkLine": true,
"dataLabel": false,
"xAxis": {
"labelCount": 4,
"itemCount": 40,
"disableGrid": true,
"gridColor": "#CCCCCC",
"gridType": "solid",
"dashLength": 4,
"scrollShow": true,
"scrollAlign": "left",
"scrollColor": "#A6A6A6",
"scrollBackgroundColor": "#EFEBEF"
},
"yAxis": {
},
"legend": {
},
"extra": {
"candle": {
"color": {
"upLine": "#f04864",
"upFill": "#f04864",
"downLine": "#2fc25b",
"downFill": "#2fc25b"
},
"average": {
"show": true,
"name": ["MA5","MA10","MA30"],
"day": [5,10,20],
"color": ["#1890ff","#2fc25b","#facc14"]
}
},
"markLine": {
"type": "dash",
"dashLength": 5,
"data": [
{
"value": 2150,
"lineColor": "#f04864",
"showLabel": true
},
{
"value": 2350,
"lineColor": "#f04864",
"showLabel": true
}
]
}
}
},
"mix":{
"type": "mix",
"color": color,
"padding": [15,15,0,15],
"xAxis": {
"disableGrid": true,
},
"yAxis": {
"disabled": false,
"disableGrid": false,
"splitNumber": 5,
"gridType": "dash",
"dashLength": 4,
"gridColor": "#CCCCCC",
"padding": 10,
"showTitle": true,
"data": []
},
"legend": {
},
"extra": {
"mix": {
"column": {
"width": 20
}
},
}
},
"point":{
"type": "point",
"color":color,
"padding":[15,15,0,15],
},
"bubble":{
"type": "bubble",
"color":color,
"padding":[15,15,0,15],
}
}

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,12 @@
# uCharts JSSDK说明
1、如不使用uCharts组件可直接引用u-charts.js打包编译后会`自动压缩`,压缩后体积约为`98kb`
2、如果100kb的体积仍需压缩请手动删除u-charts.js内您不需要的图表类型如k线图candle。
3、config-ucharts.js为uCharts组件的用户配置文件升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。
3、config-echarts.js为ECharts组件的用户配置文件升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。
# v1.0转v2.0注意事项
1、opts.colors变更为opts.color
2、ring圆环图的扩展配置由extra.pie变更为extra.ring
3、混合图借用的扩展配置由extra.column变更为extra.mix.column
4、全部涉及到format的格式化属性变更为formatter
5、不需要再传canvasId及$this参数如果通过uChats获取context可能会导致this实例混乱导致小程序开发者工具报错。如果不使用qiun-data-charts官方组件需要在new uCharts()实例化之前自行获取canvas的上下文contextctx并传入new中的contextopts.context。为了能跨更多的端给您带来的不便敬请谅解。

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,563 @@
export default {
props: {
localdata: {
type: [Array, Object],
default () {
return []
}
},
spaceInfo: {
type: Object,
default () {
return {}
}
},
collection: {
type: String,
default: ''
},
action: {
type: String,
default: ''
},
field: {
type: String,
default: ''
},
orderby: {
type: String,
default: ''
},
where: {
type: [String, Object],
default: ''
},
pageData: {
type: String,
default: 'add'
},
pageCurrent: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 20
},
getcount: {
type: [Boolean, String],
default: false
},
getone: {
type: [Boolean, String],
default: false
},
gettree: {
type: [Boolean, String],
default: false
},
manual: {
type: Boolean,
default: false
},
value: {
type: [Array, String, Number],
default () {
return []
}
},
modelValue: {
type: [Array, String, Number],
default () {
return []
}
},
preload: {
type: Boolean,
default: false
},
stepSearh: {
type: Boolean,
default: true
},
selfField: {
type: String,
default: ''
},
parentField: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: false
},
map: {
type: Object,
default() {
return {
text: "text",
value: "value"
}
}
}
},
data() {
return {
loading: false,
errorMessage: '',
loadMore: {
contentdown: '',
contentrefresh: '',
contentnomore: ''
},
dataList: [],
selected: [],
selectedIndex: 0,
page: {
current: this.pageCurrent,
size: this.pageSize,
count: 0
}
}
},
computed: {
isLocaldata() {
return !this.collection.length
},
postField() {
let fields = [this.field];
if (this.parentField) {
fields.push(`${this.parentField} as parent_value`);
}
return fields.join(',');
},
dataValue() {
let isModelValue = Array.isArray(this.modelValue) ? (this.modelValue.length > 0) : (this.modelValue !== null || this.modelValue !== undefined)
return isModelValue ? this.modelValue : this.value
},
hasValue() {
if (typeof this.dataValue === 'number') {
return true
}
return (this.dataValue != null) && (this.dataValue.length > 0)
}
},
created() {
this.$watch(() => {
var al = [];
['pageCurrent',
'pageSize',
'spaceInfo',
'value',
'modelValue',
'localdata',
'collection',
'action',
'field',
'orderby',
'where',
'getont',
'getcount',
'gettree'
].forEach(key => {
al.push(this[key])
});
return al
}, (newValue, oldValue) => {
let needReset = false
for (let i = 2; i < newValue.length; i++) {
if (newValue[i] != oldValue[i]) {
needReset = true
break
}
}
if (newValue[0] != oldValue[0]) {
this.page.current = this.pageCurrent
}
this.page.size = this.pageSize
this.onPropsChange()
})
this._treeData = []
},
methods: {
onPropsChange() {
this._treeData = []
},
getCommand(options = {}) {
/* eslint-disable no-undef */
let db = uniCloud.database(this.spaceInfo)
const action = options.action || this.action
if (action) {
db = db.action(action)
}
const collection = options.collection || this.collection
db = db.collection(collection)
const where = options.where || this.where
if (!(!where || !Object.keys(where).length)) {
db = db.where(where)
}
const field = options.field || this.field
if (field) {
db = db.field(field)
}
const orderby = options.orderby || this.orderby
if (orderby) {
db = db.orderBy(orderby)
}
const current = options.pageCurrent !== undefined ? options.pageCurrent : this.page.current
const size = options.pageSize !== undefined ? options.pageSize : this.page.size
const getCount = options.getcount !== undefined ? options.getcount : this.getcount
const getTree = options.gettree !== undefined ? options.gettree : this.gettree
const getOptions = {
getCount,
getTree
}
if (options.getTreePath) {
getOptions.getTreePath = options.getTreePath
}
db = db.skip(size * (current - 1)).limit(size).get(getOptions)
return db
},
getNodeData(callback) {
if (this.loading) {
return
}
this.loading = true
this.getCommand({
field: this.postField,
where: this._pathWhere()
}).then((res) => {
this.loading = false
this.selected = res.result.data
callback && callback()
}).catch((err) => {
this.loading = false
this.errorMessage = err
})
},
getTreePath(callback) {
if (this.loading) {
return
}
this.loading = true
this.getCommand({
field: this.postField,
getTreePath: {
startWith: `${this.selfField}=='${this.dataValue}'`
}
}).then((res) => {
this.loading = false
let treePath = []
this._extractTreePath(res.result.data, treePath)
this.selected = treePath
callback && callback()
}).catch((err) => {
this.loading = false
this.errorMessage = err
})
},
loadData() {
if (this.isLocaldata) {
this._processLocalData()
return
}
if (this.dataValue != null) {
this._loadNodeData((data) => {
this._treeData = data
this._updateBindData()
this._updateSelected()
})
return
}
if (this.stepSearh) {
this._loadNodeData((data) => {
this._treeData = data
this._updateBindData()
})
} else {
this._loadAllData((data) => {
this._treeData = []
this._extractTree(data, this._treeData, null)
this._updateBindData()
})
}
},
_loadAllData(callback) {
if (this.loading) {
return
}
this.loading = true
this.getCommand({
field: this.postField,
gettree: true,
startwith: `${this.selfField}=='${this.dataValue}'`
}).then((res) => {
this.loading = false
callback(res.result.data)
this.onDataChange()
}).catch((err) => {
this.loading = false
this.errorMessage = err
})
},
_loadNodeData(callback, pw) {
if (this.loading) {
return
}
this.loading = true
this.getCommand({
field: this.postField,
where: pw || this._postWhere(),
pageSize: 500
}).then((res) => {
this.loading = false
callback(res.result.data)
this.onDataChange()
}).catch((err) => {
this.loading = false
this.errorMessage = err
})
},
_pathWhere() {
let result = []
let where_field = this._getParentNameByField();
if (where_field) {
result.push(`${where_field} == '${this.dataValue}'`)
}
if (this.where) {
return `(${this.where}) && (${result.join(' || ')})`
}
return result.join(' || ')
},
_postWhere() {
let result = []
let selected = this.selected
let parentField = this.parentField
if (parentField) {
result.push(`${parentField} == null || ${parentField} == ""`)
}
if (selected.length) {
for (var i = 0; i < selected.length - 1; i++) {
result.push(`${parentField} == '${selected[i].value}'`)
}
}
let where = []
if (this.where) {
where.push(`(${this.where})`)
}
if (result.length) {
where.push(`(${result.join(' || ')})`)
}
return where.join(' && ')
},
_nodeWhere() {
let result = []
let selected = this.selected
if (selected.length) {
result.push(`${this.parentField} == '${selected[selected.length - 1].value}'`)
}
if (this.where) {
return `(${this.where}) && (${result.join(' || ')})`
}
return result.join(' || ')
},
_getParentNameByField() {
const fields = this.field.split(',');
let where_field = null;
for (let i = 0; i < fields.length; i++) {
const items = fields[i].split('as');
if (items.length < 2) {
continue;
}
if (items[1].trim() === 'value') {
where_field = items[0].trim();
break;
}
}
return where_field
},
_isTreeView() {
return (this.parentField && this.selfField)
},
_updateSelected() {
var dl = this.dataList
var sl = this.selected
let textField = this.map.text
let valueField = this.map.value
for (var i = 0; i < sl.length; i++) {
var value = sl[i].value
var dl2 = dl[i]
for (var j = 0; j < dl2.length; j++) {
var item2 = dl2[j]
if (item2[valueField] === value) {
sl[i].text = item2[textField]
break
}
}
}
},
_updateBindData(node) {
const {
dataList,
hasNodes
} = this._filterData(this._treeData, this.selected)
let isleaf = this._stepSearh === false && !hasNodes
if (node) {
node.isleaf = isleaf
}
this.dataList = dataList
this.selectedIndex = dataList.length - 1
if (!isleaf && this.selected.length < dataList.length) {
this.selected.push({
value: null,
text: "请选择"
})
}
return {
isleaf,
hasNodes
}
},
_filterData(data, paths) {
let dataList = []
let hasNodes = true
dataList.push(data.filter((item) => {
return (item.parent_value === null || item.parent_value === undefined || item.parent_value === '')
}))
for (let i = 0; i < paths.length; i++) {
var value = paths[i].value
var nodes = data.filter((item) => {
return item.parent_value === value
})
if (nodes.length) {
dataList.push(nodes)
} else {
hasNodes = false
}
}
return {
dataList,
hasNodes
}
},
_extractTree(nodes, result, parent_value) {
let list = result || []
let valueField = this.map.value
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
let child = {}
for (let key in node) {
if (key !== 'children') {
child[key] = node[key]
}
}
if (parent_value !== null && parent_value !== undefined && parent_value !== '') {
child.parent_value = parent_value
}
result.push(child)
let children = node.children
if (children) {
this._extractTree(children, result, node[valueField])
}
}
},
_extractTreePath(nodes, result) {
let list = result || []
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
let child = {}
for (let key in node) {
if (key !== 'children') {
child[key] = node[key]
}
}
result.push(child)
let children = node.children
if (children) {
this._extractTreePath(children, result)
}
}
},
_findNodePath(key, nodes, path = []) {
let textField = this.map.text
let valueField = this.map.value
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
let children = node.children
let text = node[textField]
let value = node[valueField]
path.push({
value,
text
})
if (value === key) {
return path
}
if (children) {
const p = this._findNodePath(key, children, path)
if (p.length) {
return p
}
}
path.pop()
}
return []
},
_processLocalData() {
this._treeData = []
this._extractTree(this.localdata, this._treeData)
var inputValue = this.dataValue
if (inputValue === undefined) {
return
}
if (Array.isArray(inputValue)) {
inputValue = inputValue[inputValue.length - 1]
if (typeof inputValue === 'object' && inputValue[this.map.value]) {
inputValue = inputValue[this.map.value]
}
}
this.selected = this._findNodePath(inputValue, this.localdata)
}
}
}

View File

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

View File

@@ -0,0 +1,435 @@
<template>
<view class="uni-stat__select">
<span v-if="label" class="uni-label-text hide-on-phone">{{label + ''}}</span>
<view class="uni-stat-box" :class="{'uni-stat__actived': current}">
<view class="uni-select" :class="{'uni-select--disabled':disabled}">
<view class="uni-select__input-box" @click="toggleSelector">
<view v-if="current" class="uni-select__input-text">{{current}}</view>
<view v-else class="uni-select__input-text uni-select__input-placeholder">{{typePlaceholder}}</view>
<uni-icons v-if="current && clear" type="clear" color="#c0c4cc" size="24" @click="clearVal" />
<uni-icons v-else :type="showSelector? 'top' : 'bottom'" size="14" color="#999" />
</view>
<view class="uni-select--mask" v-if="showSelector" @click="toggleSelector" />
<view class="uni-select__selector" v-if="showSelector">
<view class="uni-popper__arrow"></view>
<scroll-view scroll-y="true" class="uni-select__selector-scroll">
<view class="uni-select__selector-empty" v-if="mixinDatacomResData.length === 0">
<text>{{emptyTips}}</text>
</view>
<view v-else class="uni-select__selector-item" v-for="(item,index) in mixinDatacomResData" :key="index" @click="change(item)">
<text :class="{'uni-select__selector__disabled': item.disable}">{{formatItemName(item)}}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script>
/**
* DataChecklist 数据选择器
* @description 通过数据渲染的下拉框组件
* @tutorial https://uniapp.dcloud.io/component/uniui/uni-data-select
* @property {String} value 默认值
* @property {Array} localdata 本地数据 ,格式 [{text:'',value:''}]
* @property {Boolean} clear 是否可以清空已选项
* @property {Boolean} emptyText 没有数据时显示的文字 ,本地数据无效
* @property {String} label 左侧标题
* @property {String} placeholder 输入框的提示文字
* @property {Boolean} disabled 是否禁用
* @event {Function} change 选中发生变化触发
*/
export default {
name: "uni-stat-select",
mixins: [uniCloud.mixinDatacom || {}],
data() {
return {
showSelector: false,
current: '',
mixinDatacomResData: [],
apps: [],
channels: []
};
},
props: {
localdata: {
type: Array,
default() {
return []
}
},
value: {
type: [String, Number],
default: ''
},
modelValue: {
type: [String, Number],
default: ''
},
label: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '请选择'
},
emptyTips: {
type: String,
default: '无选项'
},
clear: {
type: Boolean,
default: true
},
defItem: {
type: Number,
default: 0
},
disabled: {
type: Boolean,
default: false
}
},
created() {
this.last = `${this.collection}_last_selected_option_value`
if (this.collection && !this.localdata.length) {
this.mixinDatacomEasyGet()
}
},
computed: {
typePlaceholder() {
const text = {
'opendb-stat-app-versions': '版本',
'opendb-app-channels': '渠道',
'opendb-app-list': '应用'
}
const common = this.placeholder
const placeholder = text[this.collection]
return placeholder ?
common + placeholder :
common
}
},
watch: {
localdata: {
immediate: true,
handler(val, old) {
if (Array.isArray(val) && old !== val) {
this.mixinDatacomResData = val
}
}
},
// #ifndef VUE3
value() {
this.initDefVal()
},
// #endif
// #ifdef VUE3
modelValue() {
this.initDefVal()
},
// #endif
mixinDatacomResData: {
immediate: true,
handler(val) {
if (val.length) {
this.initDefVal()
}
}
}
},
methods: {
initDefVal() {
let defValue = ''
if ((this.value || this.value === 0) && !this.isDisabled(this.value)) {
defValue = this.value
} else if ((this.modelValue || this.modelValue === 0) && !this.isDisabled(this.modelValue)) {
defValue = this.modelValue
} else {
let strogeValue
if (this.collection) {
strogeValue = uni.getStorageSync(this.last)
}
if (strogeValue || strogeValue === 0) {
defValue = strogeValue
} else {
let defItem = ''
if (this.defItem > 0 && this.defItem < this.mixinDatacomResData.length) {
defItem = this.mixinDatacomResData[this.defItem - 1].value
}
defValue = defItem
}
this.emit(defValue)
}
const def = this.mixinDatacomResData.find(item => item.value === defValue)
this.current = def ? this.formatItemName(def) : ''
},
/**
* @param {[String, Number]} value
* 判断用户给的 value 是否同时为禁用状态
*/
isDisabled(value) {
let isDisabled = false;
this.mixinDatacomResData.forEach(item => {
if (item.value === value) {
isDisabled = item.disable
}
})
return isDisabled;
},
clearVal() {
this.emit('')
if (this.collection) {
uni.removeStorageSync(this.last)
}
},
change(item) {
if (!item.disable) {
this.showSelector = false
this.current = this.formatItemName(item)
this.emit(item.value)
}
},
emit(val) {
this.$emit('change', val)
this.$emit('input', val)
this.$emit('update:modelValue', val)
if (this.collection) {
uni.setStorageSync(this.last, val)
}
},
toggleSelector() {
if (this.disabled) {
return
}
this.showSelector = !this.showSelector
},
formatItemName(item) {
let {
text,
value,
channel_code
} = item
channel_code = channel_code ? `(${channel_code})` : ''
return this.collection.indexOf('app-list') > 0 ?
`${text}(${value})` :
(
text ? text : `未命名${channel_code}`
)
}
}
}
</script>
<style lang="scss">
$uni-base-color: #6a6a6a !default;
$uni-main-color: #333 !default;
$uni-secondary-color: #909399 !default;
$uni-border-3: #e5e5e5;
/* #ifndef APP-NVUE */
@media screen and (max-width: 500px) {
.hide-on-phone {
display: none;
}
}
/* #endif */
.uni-stat__select {
display: flex;
align-items: center;
// padding: 15px;
cursor: pointer;
width: 100%;
flex: 1;
box-sizing: border-box;
}
.uni-stat-box {
width: 100%;
flex: 1;
}
.uni-stat__actived {
width: 100%;
flex: 1;
// outline: 1px solid #2979ff;
}
.uni-label-text {
font-size: 14px;
font-weight: bold;
color: $uni-base-color;
margin: auto 0;
margin-right: 5px;
}
.uni-select {
font-size: 14px;
border: 1px solid $uni-border-3;
box-sizing: border-box;
border-radius: 4px;
padding: 0 5px;
padding-left: 10px;
position: relative;
/* #ifndef APP-NVUE */
display: flex;
user-select: none;
/* #endif */
flex-direction: row;
align-items: center;
border-bottom: solid 1px $uni-border-3;
width: 100%;
flex: 1;
height: 35px;
&--disabled{
background-color: #f5f7fa;
cursor: not-allowed;
}
}
.uni-select__label {
font-size: 16px;
// line-height: 22px;
height: 35px;
padding-right: 10px;
color: $uni-secondary-color;
}
.uni-select__input-box {
// height: 35px;
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
}
.uni-select__input {
flex: 1;
font-size: 14px;
height: 22px;
line-height: 22px;
}
.uni-select__input-plac {
font-size: 14px;
color: $uni-secondary-color;
}
.uni-select__selector {
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
position: absolute;
top: calc(100% + 12px);
left: 0;
width: 100%;
background-color: #FFFFFF;
border: 1px solid #EBEEF5;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 2;
padding: 4px 0;
}
.uni-select__selector-scroll {
/* #ifndef APP-NVUE */
max-height: 200px;
box-sizing: border-box;
/* #endif */
}
.uni-select__selector-empty,
.uni-select__selector-item {
/* #ifndef APP-NVUE */
display: flex;
cursor: pointer;
/* #endif */
line-height: 35px;
font-size: 14px;
text-align: center;
/* border-bottom: solid 1px $uni-border-3; */
padding: 0px 10px;
}
.uni-select__selector-item:hover {
background-color: #f9f9f9;
}
.uni-select__selector-empty:last-child,
.uni-select__selector-item:last-child {
/* #ifndef APP-NVUE */
border-bottom: none;
/* #endif */
}
.uni-select__selector__disabled {
opacity: 0.4;
cursor: default;
}
/* picker 弹出层通用的指示小三角 */
.uni-popper__arrow,
.uni-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
}
.uni-popper__arrow {
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
top: -6px;
left: 10%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #EBEEF5;
}
.uni-popper__arrow::after {
content: " ";
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
.uni-select__input-text {
// width: 280px;
width: 100%;
color: $uni-main-color;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
overflow: hidden;
}
.uni-select__input-placeholder {
color: $uni-base-color;
font-size: 12px;
}
.uni-select--mask {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
</style>

View File

@@ -0,0 +1,416 @@
<template>
<view class="table-container">
<view class="thead">
<view class="th" v-for="(th, thIndex) in cols" :key="thIndex" :style="{ flex: th.width, maxWidth: th.width + '%', textAlign: th.align ? th.align : 'center' }">
<view class="content">
<block v-if="th.checkbox">
<view class="all-select" @mouseenter="allSelectShow()" @mouseleave="allSelectHide()">
<view class="all-select-label">
<text>全选<text v-if="allSelect.num > 0">({{ allSelect.num }})</text></text>
<text class="iconfont iconxiala"></text>
</view>
<view class="all-select-option" :style="allSelect.show? 'display:block;' : ''">
<view
v-for="(oVal) in allSelect.optionList"
:class="allSelect.hoverOption==oVal.id? 'on' : ''"
@mouseenter="allSelect.hoverOption = oVal.id"
@click="allSelectClick(oVal)"
>{{ oVal.name }}
<text v-if="(oVal.id == 'curr' && allSelect.pageSelected[page]) || (oVal.id == 'all' && allSelect.selected)" style="color:red;margin-left: 4px;"></text>
</view>
</view>
</view>
</block>
<text v-else>{{ th.title }}</text>
</view>
</view>
</view>
<view class="tbody">
<view class="tr" v-for="(d, index) in list" :key="index" v-if="list.length">
<view class="td" v-for="(th, thIndex) in cols" :key="thIndex" :style="{ flex: th.width, maxWidth: th.width + '%', textAlign: th.align ? th.align : 'center' }">
<view class="content" :class="{ action: th.action }">
<view v-if="th.checkbox">
<text v-if="typeof th.disabled == 'function' && th.disabled(d)" class="iconfont iconfuxuankuang2 disabled"></text>
<text v-else @click="single(d, index)" class="iconfont" :class="{
iconfuxuankuang2: Boolean(selectedData[d[pk]]) == false,
iconfuxuankuang1: Boolean(selectedData[d[pk]]) == true,
}"></text>
</view>
<slot v-else-if="th.action" name="action" :value="d" :index="index"></slot>
<view v-else-if="th.templet" v-html="th.templet(d)"></view>
<view v-else-if="th.return">{{ th.return(d) }}</view>
<view v-else>{{ d[th.field] }}</view>
</view>
</view>
</view>
<view class="tr empty" v-if="!list.length">
<view class="td">
<view class="iconfont iconwushuju"></view>
<view>暂无数据</view>
</view>
</view>
</view>
<view class="tpage" v-if="list.length && classType == false">
<view class="batch-action">
<slot name="batchaction" :value="selected"></slot>
</view>
<uni-pagination :total="total" :showIcon="true" @change="pageChange" :pageSize="pagesize" :value="page" />
</view>
</view>
</template>
<script>
export default {
name: 'uniDataTableNew',
props: {
cols: {
type: Array
},
url: {
type: String,
default: ''
},
pagesize: {
type: Number,
default: 10
},
option: {
type: Object,
default: function () {
return {};
}
},
classType: {
type: Boolean,
default: false
},
data: {
type: Object | Array,
default: function () {
return {};
}
},
pk: {//主键id
type: String,
default: ''
},
},
created() {
this.url && this.load({page:1});
},
data() {
return {
list: [],
selected: [],
selectedIndex: [],
selectedData:{},
unselectedData:{},
page: 1,
total: 0,
pageCount:0,
allSelect:{
optionList:[
{id:'curr',name:'当前页'},
{id:'all',name:'所有页'},
],
show:false,
num:0,
hoverOption:'',
selected:false,//所有页选中
pageSelected:{},
},
};
},
watch: {
data: {
handler(nVal, oVal) {
if (Object.keys(nVal).length) this.list = Object.values(nVal)
},
deep: true,
immediate: true
}
},
methods: {
allSelectShow(){
this.allSelect.show = true;
},
allSelectHide(){
this.allSelect.show = false;
this.allSelect.hoverOption = '';
},
allSelectInitData(){
this.allSelect.pageSelected = {};
this.allSelect.selected = false;
if(this.pageCount > 0){
for(let i = 1; i < this.pageCount; i++){
this.allSelect.pageSelected[i] = false;
}
}
this.selectedData = {};
this.unselectedData = {};
this.allSelect.num = 0;
},
allSelectClick(item){
if(item.id == 'curr'){
this.allSelect.pageSelected[this.page] = !this.allSelect.pageSelected[this.page];
}else{
this.allSelect.selected = !this.allSelect.selected;
for(let i in this.allSelect.pageSelected){
this.allSelect.pageSelected[i] = this.allSelect.selected;
}
if(!this.allSelect.selected){
this.selectedData = {};
this.unselectedData = {};
}
}
this.handleSelectData();
this.allSelectNum();
this.allSelectHide();
},
allSelectNum(){
if(this.allSelect.selected){
this.allSelect.num = this.total - Object.values(this.unselectedData).length;
}else{
this.allSelect.num = Object.values(this.selectedData).length;
}
},
/**
* 全选
*/
handleSelectData() {
if (this.list.length) {
let firstTh = this.cols[0];
if(this.allSelect.pageSelected[this.page]){
this.list.forEach((item)=>{
if (typeof firstTh.disabled == 'function' && firstTh.disabled(item)) return;
if(!this.selectedData[item[this.pk]]){
this.selectedData[item[this.pk]] = item;
}
if(this.unselectedData[item[this.pk]]){
delete this.unselectedData[item[this.pk]];
}
})
}else{
this.list.forEach((item)=>{
if (typeof firstTh.disabled == 'function' && firstTh.disabled(item)) return;
if(!this.unselectedData[item[this.pk]]){
this.unselectedData[item[this.pk]] = item;
}
if(this.selectedData[item[this.pk]]){
delete this.selectedData[item[this.pk]];
}
})
}
}
},
/**
* 单选
* @param {Object} item
* @param {Object} index
*/
single(item, index) {
if(this.selectedData[item[this.pk]]){
delete this.selectedData[item[this.pk]];
}else{
this.selectedData[item[this.pk]] = item;
}
if(this.unselectedData[item[this.pk]]){
delete this.unselectedData[item[this.pk]];
}else{
this.unselectedData[item[this.pk]] = item;
}
this.allSelectNum();
},
/**
* 设置默认选中数据
*/
defaultSelectData(selected, selectedIndex) {
this.selected = selected;
this.selectedIndex = selectedIndex;
},
pageChange(e) {
this.page = e.current;
this.load();
this.$emit('pageChange', this.page);
},
load(option = {}) {
let data = {
page: option.page || this.page,
page_size: this.pagesize
};
if (this.option) Object.assign(data, this.option);
if (option) Object.assign(data, option);
this.$api.sendRequest({
url: this.url,
data: data,
success: res => {
if (res.code >= 0) {
this.list = res.data.list;
this.total = res.data.count;
this.pageCount = res.data.page_count;
this.selected = [];
this.selectedIndex = [];
this.$emit('tableData', this.list);
if (option.page) {
this.page = option.page;
if(option.page == 1){
this.allSelectInitData();
}
delete option.page;
}
this.handleSelectData();
} else {
this.$util.showToast({ title: res.message });
}
},
fail: () => {
this.$util.showToast({ title: '请求失败' });
}
});
},
//库存页面清除选中
clearCheck() {
this.allSelectInitData();
},
//返回选择数据
getSelectData(){
return {
selectedData: this.selectedData,
unselectedData: this.unselectedData,
allSelected: this.allSelect.selected,
selectedNum: this.allSelect.num,
}
},
}
};
</script>
<style lang="scss">
.table-container {
width: 100%;
.iconcheckbox_weiquanxuan,
.iconfuxuankuang1,
.iconfuxuankuang2 {
color: $primary-color;
cursor: pointer;
font-size: 0.16rem;
transition: all 0.3s;
}
.iconfuxuankuang2 {
color: #e6e6e6;
&:hover {
color: $primary-color;
}
}
.disabled {
background: #eee;
cursor: not-allowed;
&:hover {
color: #e6e6e6;
}
}
}
.thead {
display: flex;
width: 100%;
height: 0.5rem;
background: #f7f8fa;
align-items: center;
.th {
padding: 0 0.1rem;
box-sizing: border-box;
.content {
white-space: nowrap;
width: 100%;
/* overflow: hidden; */
text-overflow: ellipsis;
.all-select{
position: relative;
.all-select-label{
cursor: pointer;
padding:4px;
}
.all-select-option{
position: absolute;
display: none;
left:50%;
transform: translateX(-50%);
background: #fff;
padding: 4px 0;
border-radius: 2px;
box-shadow: 0px 0px 2px #ccc;
view{
color: #000;
cursor:pointer;
margin: 4px 0;
padding:4px 20px;
&.on{
background: #eee;
}
}
}
}
}
}
}
.tr {
display: flex;
border-bottom: 0.01rem solid #e6e6e6;
min-height: 0.5rem;
align-items: center;
transition: background-color 0.3s;
padding: 0.1rem 0;
box-sizing: border-box;
&:hover {
background: #f5f5f5;
}
.td {
padding: 0 0.1rem;
box-sizing: border-box;
.content {
width: 100%;
white-space: normal;
}
}
&.empty {
justify-content: center;
.td {
text-align: center;
color: #909399;
.iconfont {
font-size: 0.25rem;
margin: 0.05rem;
}
}
}
}
.tpage {
display: flex;
align-items: center;
padding: 0.1rem 0;
margin-bottom: 0.1rem;
.uni-pagination {
justify-content: flex-end;
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,294 @@
<template>
<view class="table-container">
<view class="thead">
<view class="th" v-for="(th, thIndex) in cols" :key="thIndex" :style="{ flex: th.width, maxWidth: th.width + '%', textAlign: th.align ? th.align : 'center' }">
<view class="content">
<view v-if="th.checkbox" class="iconfont" @click="all" :class="{
iconfuxuankuang2: selected.length == 0,
iconcheckbox_weiquanxuan: selected.length != list.length,
iconfuxuankuang1: selected.length > 0 && selected.length == list.length
}"></view>
<text v-else>{{ th.title }}</text>
</view>
</view>
</view>
<view class="tbody">
<view class="tr" v-for="(d, index) in list" :key="index" v-if="list.length">
<view class="td" v-for="(th, thIndex) in cols" :key="thIndex" :style="{ flex: th.width, maxWidth: th.width + '%', textAlign: th.align ? th.align : 'center' }">
<view class="content" :class="{ action: th.action }">
<view v-if="th.checkbox">
<text v-if="typeof th.disabled == 'function' && th.disabled(d)" class="iconfont iconfuxuankuang2 disabled"></text>
<text v-else @click="single(d, index)" class="iconfont" :class="{
iconfuxuankuang2: selectedIndex.indexOf(index) == -1,
iconfuxuankuang1: selectedIndex.indexOf(index) != -1
}"></text>
</view>
<slot v-else-if="th.action" name="action" :value="d" :index="index"></slot>
<view v-else-if="th.templet" v-html="th.templet(d)"></view>
<view v-else-if="th.return">{{ th.return(d) }}</view>
<view v-else>{{ d[th.field] }}</view>
</view>
</view>
</view>
<view class="tr empty" v-if="!list.length">
<view class="td">
<view class="iconfont iconwushuju"></view>
<view>暂无数据</view>
</view>
</view>
</view>
<view class="tpage" v-if="list.length && classType == false">
<view class="batch-action">
<slot name="batchaction" :value="selected"></slot>
</view>
<uni-pagination :total="total" :showIcon="true" @change="pageChange" :pageSize="pagesize" :value="page" />
</view>
</view>
</template>
<script>
export default {
name: 'uniDataTable',
props: {
cols: {
type: Array
},
url: {
type: String,
default: ''
},
pagesize: {
type: Number,
default: 10
},
option: {
type: Object,
default: function () {
return {};
}
},
classType: {
type: Boolean,
default: false
},
data: {
type: Object | Array,
default: function () {
return {};
}
}
},
created() {
this.url && this.load();
},
data() {
return {
list: [],
selected: [],
selectedIndex: [],
page: 1,
total: 0
};
},
watch: {
data: {
handler(nVal, oVal) {
if (Object.keys(nVal).length) this.list = Object.values(nVal)
},
deep: true,
immediate: true
}
},
methods: {
/**
* 全选
*/
all() {
if (this.list.length) {
if (this.selected.length == this.list.length) {
this.selected = [];
this.selectedIndex = [];
} else {
let selectedIndex = [],
selected = [],
firstTh = this.cols[0];
this.list.forEach((item, index) => {
if (typeof firstTh.disabled == 'function' && firstTh.disabled(item)) return;
selectedIndex.push(index);
selected.push(item);
});
this.selectedIndex = selectedIndex;
this.selected = selected;
}
this.$emit('checkBox', this.selected);
}
},
/**
* 单选
* @param {Object} item
* @param {Object} index
*/
single(item, index) {
let _index = this.selectedIndex.indexOf(index);
if (_index == -1) {
this.selectedIndex.push(index);
this.selected.push(item);
} else {
this.selectedIndex.splice(_index, 1);
this.selected.splice(_index, 1);
}
this.$emit('checkBox', this.selected, this.selectedIndex);
},
/**
* 设置默认选中数据
*/
defaultSelectData(selected, selectedIndex) {
this.selected = selected;
this.selectedIndex = selectedIndex;
},
pageChange(e) {
this.page = e.current;
this.load();
this.$emit('pageChange', this.page);
},
load(option = {}) {
let data = {
page: option.page || this.page,
page_size: this.pagesize
};
if (this.option) Object.assign(data, this.option);
if (option) Object.assign(data, option);
this.$api.sendRequest({
url: this.url,
data: data,
success: res => {
if (res.code >= 0) {
this.list = res.data.list;
this.total = res.data.count;
this.selected = [];
this.selectedIndex = [];
this.$emit('tableData', this.list);
if (option.page) {
this.page = option.page;
delete option.page;
}
} else {
this.$util.showToast({ title: res.message });
}
},
fail: () => {
this.$util.showToast({ title: '请求失败' });
}
});
},
clearCheck() {//库存页面清除选中
this.selected = [];
this.selectedIndex = [];
}
}
};
</script>
<style lang="scss">
.table-container {
width: 100%;
.iconcheckbox_weiquanxuan,
.iconfuxuankuang1,
.iconfuxuankuang2 {
color: $primary-color;
cursor: pointer;
font-size: 0.16rem;
transition: all 0.3s;
}
.iconfuxuankuang2 {
color: #e6e6e6;
&:hover {
color: $primary-color;
}
}
.disabled {
background: #eee;
cursor: not-allowed;
&:hover {
color: #e6e6e6;
}
}
}
.thead {
display: flex;
width: 100%;
height: 0.5rem;
background: #f7f8fa;
align-items: center;
.th {
padding: 0 0.1rem;
box-sizing: border-box;
.content {
white-space: nowrap;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.tr {
display: flex;
border-bottom: 0.01rem solid #e6e6e6;
min-height: 0.5rem;
align-items: center;
transition: background-color 0.3s;
padding: 0.1rem 0;
box-sizing: border-box;
&:hover {
background: #f5f5f5;
}
.td {
padding: 0 0.1rem;
box-sizing: border-box;
.content {
width: 100%;
white-space: normal;
}
}
&.empty {
justify-content: center;
.td {
text-align: center;
color: #909399;
.iconfont {
font-size: 0.25rem;
margin: 0.05rem;
}
}
}
}
.tpage {
display: flex;
align-items: center;
padding: 0.1rem 0;
margin-bottom: 0.1rem;
.uni-pagination {
justify-content: flex-end;
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,183 @@
<template>
<view class="uni-calendar-item__weeks-box" :class="{
'uni-calendar-item--disable':weeks.disable,
'uni-calendar-item--before-checked-x':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked-x':weeks.afterMultiple,
}" @click="choiceDate(weeks)" @mouseenter="handleMousemove(weeks)">
<view class="uni-calendar-item__weeks-box-item" :class="{
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && (calendar.userChecked || !checkHover),
'uni-calendar-item--checked-range-text': checkHover,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">
<text v-if="selected&&weeks.extraInfo" class="uni-calendar-item__weeks-box-circle"></text>
<text class="uni-calendar-item__weeks-box-text uni-calendar-item__weeks-box-text-disable uni-calendar-item--checked-text">{{weeks.date}}</text>
</view>
<view :class="{'uni-calendar-item--isDay': weeks.isDay}"></view>
</view>
</template>
<script>
export default {
props: {
weeks: {
type: Object,
default () {
return {}
}
},
calendar: {
type: Object,
default: () => {
return {}
}
},
selected: {
type: Array,
default: () => {
return []
}
},
lunar: {
type: Boolean,
default: false
},
checkHover: {
type: Boolean,
default: false
}
},
methods: {
choiceDate(weeks) {
this.$emit('change', weeks)
},
handleMousemove(weeks) {
this.$emit('handleMouse', weeks)
}
}
}
</script>
<style lang="scss" scoped>
.uni-calendar-item__weeks-box {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
margin: 0.01rem 0;
position: relative;
}
.uni-calendar-item__weeks-box-text {
font-size: 0.14rem;
// font-family: Lato-Bold, Lato;
font-weight: bold;
color: #455997;
}
.uni-calendar-item__weeks-lunar-text {
font-size: 0.12rem;
color: #333;
}
.uni-calendar-item__weeks-box-item {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
width: 0.4rem;
height: 0.4rem;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-calendar-item__weeks-box-circle {
position: absolute;
top: 0.05rem;
right: 0.05rem;
width: 0.08rem;
height: 0.08rem;
border-radius: 0.08rem;
background-color: #dd524d;
}
.uni-calendar-item__weeks-box .uni-calendar-item--disable {
// background-color: rgba(249, 249, 249, $uni-opacity-disabled);
cursor: default;
}
.uni-calendar-item--disable .uni-calendar-item__weeks-box-text-disable {
color: #D1D1D1;
}
.uni-calendar-item--isDay {
position: absolute;
top: 0.1rem;
right: 17%;
background-color: #dd524d;
width: 0.06rem;
height: 0.06rem;
border-radius: 50%;
}
.uni-calendar-item--extra {
color: #dd524d;
opacity: 0.8;
}
.uni-calendar-item__weeks-box .uni-calendar-item--checked {
background-color: $primary-color;
border-radius: 50%;
box-sizing: border-box;
border: 0.03rem solid #fff;
}
.uni-calendar-item--checked .uni-calendar-item--checked-text {
color: #fff;
}
.uni-calendar-item--multiple .uni-calendar-item--checked-range-text {
color: #333;
}
.uni-calendar-item--multiple {
background-color: #F6F7FC;
// color: #fff;
}
.uni-calendar-item--multiple .uni-calendar-item--before-checked,
.uni-calendar-item--multiple .uni-calendar-item--after-checked {
background-color: $primary-color;
border-radius: 50%;
box-sizing: border-box;
border: 0.03rem solid #F6F7FC;
}
.uni-calendar-item--before-checked .uni-calendar-item--checked-text,
.uni-calendar-item--after-checked .uni-calendar-item--checked-text {
color: #fff;
}
.uni-calendar-item--before-checked-x {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
box-sizing: border-box;
background-color: #F6F7FC;
}
.uni-calendar-item--after-checked-x {
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
background-color: #F6F7FC;
}
</style>

View File

@@ -0,0 +1,883 @@
<template>
<view class="uni-calendar" @mouseleave="leaveCale">
<view v-if="!insert&&show" class="uni-calendar__mask" :class="{'uni-calendar--mask-show':aniMaskShow}" @click="clean"></view>
<view v-if="insert || show" class="uni-calendar__content" :class="{'uni-calendar--fixed':!insert,'uni-calendar--ani-show':aniMaskShow, 'uni-calendar__content-mobile': aniMaskShow}">
<view class="uni-calendar__header" :class="{'uni-calendar__header-mobile' :!insert}">
<view v-if="left" class="uni-calendar__header-btn-box" @click.stop="pre">
<view class="uni-calendar__header-btn uni-calendar--left"></view>
</view>
<picker mode="date" :value="date" fields="month" @change="bindDateChange">
<text class="uni-calendar__header-text">{{ (nowDate.year||'') + ' 年 ' + ( nowDate.month||'') +' 月'}}</text>
</picker>
<view v-if="right" class="uni-calendar__header-btn-box" @click.stop="next">
<view class="uni-calendar__header-btn uni-calendar--right"></view>
</view>
<view v-if="!insert" class="dialog-close" @click="clean">
<view class="dialog-close-plus" data-id="close"></view>
<view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
</view>
<!-- <text class="uni-calendar__backtoday" @click="backtoday">回到今天</text> -->
</view>
<view class="uni-calendar__box">
<view v-if="showMonth" class="uni-calendar__box-bg">
<text class="uni-calendar__box-bg-text">{{nowDate.month}}</text>
</view>
<view class="uni-calendar__weeks" style="padding-bottom: 0.07rem;">
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SUNText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{monText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{TUEText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{WEDText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{THUText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{FRIText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SATText}}</text>
</view>
</view>
<view class="uni-calendar__weeks" v-for="(item,weekIndex) in weeks" :key="weekIndex">
<view class="uni-calendar__weeks-item" v-for="(weeks,weeksIndex) in item" :key="weeksIndex">
<calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar" :selected="selected" :lunar="lunar" :checkHover="range" @change="choiceDate" @handleMouse="handleMouse"></calendar-item>
</view>
</view>
</view>
<view v-if="!insert && !range && typeHasTime" class="uni-date-changed uni-calendar--fixed-top" style="padding: 0 0.8rem;">
<view class="uni-date-changed--time-date">{{tempSingleDate ? tempSingleDate : selectDateText}}</view>
<time-picker type="time" :start="reactStartTime" :end="reactEndTime" v-model="time" :disabled="!tempSingleDate" :border="false" :hide-second="hideSecond" class="time-picker-style"></time-picker>
</view>
<view v-if="!insert && range && typeHasTime" class="uni-date-changed uni-calendar--fixed-top">
<view class="uni-date-changed--time-start">
<view class="uni-date-changed--time-date">{{tempRange.before ? tempRange.before : startDateText}}</view>
<time-picker type="time" :start="reactStartTime" v-model="timeRange.startTime" :border="false" :hide-second="hideSecond" :disabled="!tempRange.before" class="time-picker-style">
</time-picker>
</view>
<uni-icons type="arrowthinright" color="#999" style="line-height: 0.5rem;"></uni-icons>
<view class="uni-date-changed--time-end">
<view class="uni-date-changed--time-date">{{tempRange.after ? tempRange.after : endDateText}}</view>
<time-picker type="time" :end="reactEndTime" v-model="timeRange.endTime" :border="false" :hide-second="hideSecond" :disabled="!tempRange.after" class="time-picker-style">
</time-picker>
</view>
</view>
<view v-if="!insert" class="uni-date-changed uni-date-btn--ok">
<!-- <view class="uni-calendar__header-btn-box">
<text class="uni-calendar__button-text uni-calendar--fixed-width">{{okText}}</text>
</view> -->
<view class="uni-datetime-picker--btn" @click="confirm">确认</view>
</view>
</view>
</view>
</template>
<script>
import Calendar from './util.js';
import calendarItem from './calendar-item.vue'
import timePicker from './time-picker.vue'
import uniIcons from '@/components/uni-icons/uni-icons';
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const {
t
} = initVueI18n(messages)
/**
* Calendar 日历
* @description 日历组件可以查看日期,选择任意范围内的日期,打点操作。常用场景如:酒店日期预订、火车机票选择购买日期、上下班打卡等
* @tutorial https://ext.dcloud.net.cn/plugin?id=56
* @property {String} date 自定义当前时间,默认为今天
* @property {Boolean} lunar 显示农历
* @property {String} startDate 日期选择范围-开始日期
* @property {String} endDate 日期选择范围-结束日期
* @property {Boolean} range 范围选择
* @property {Boolean} insert = [true|false] 插入模式,默认为false
* @value true 弹窗模式
* @value false 插入模式
* @property {Boolean} clearDate = [true|false] 弹窗模式是否清空上次选择内容
* @property {Array} selected 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}]
* @property {Boolean} showMonth 是否选择月份为背景
* @event {Function} change 日期改变,`insert :ture` 时生效
* @event {Function} confirm 确认选择`insert :false` 时生效
* @event {Function} monthSwitch 切换月份时触发
* @example <uni-calendar :insert="true":lunar="true" :start-date="'2019-3-2'":end-date="'2019-5-20'"@change="change" />
*/
export default {
components: {
uniIcons,
calendarItem,
timePicker
},
props: {
date: {
type: String,
default: ''
},
defTime: {
type: [String, Object],
default: ''
},
selectableTimes: {
type: [Object],
default () {
return {}
}
},
selected: {
type: Array,
default () {
return []
}
},
lunar: {
type: Boolean,
default: false
},
startDate: {
type: String,
default: ''
},
endDate: {
type: String,
default: ''
},
range: {
type: Boolean,
default: false
},
typeHasTime: {
type: Boolean,
default: false
},
insert: {
type: Boolean,
default: true
},
showMonth: {
type: Boolean,
default: true
},
clearDate: {
type: Boolean,
default: true
},
left: {
type: Boolean,
default: true
},
right: {
type: Boolean,
default: true
},
checkHover: {
type: Boolean,
default: true
},
hideSecond: {
type: [Boolean],
default: false
},
pleStatus: {
type: Object,
default () {
return {
before: '',
after: '',
data: [],
fulldate: ''
}
}
}
},
data() {
return {
show: false,
weeks: [],
calendar: {},
nowDate: '',
aniMaskShow: false,
firstEnter: true,
time: '',
timeRange: {
startTime: '',
endTime: ''
},
tempSingleDate: '',
tempRange: {
before: '',
after: ''
}
}
},
watch: {
date: {
immediate: true,
handler(newVal, oldVal) {
if (!this.range) {
this.tempSingleDate = newVal
setTimeout(() => {
this.init(newVal)
}, 100)
}
}
},
defTime: {
immediate: true,
handler(newVal, oldVal) {
if (!this.range) {
this.time = newVal
} else {
this.timeRange.startTime = newVal.start
this.timeRange.endTime = newVal.end
}
}
},
startDate(val) {
this.cale.resetSatrtDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
endDate(val) {
this.cale.resetEndDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
selected(newVal) {
this.cale.setSelectInfo(this.nowDate.fullDate, newVal)
this.weeks = this.cale.weeks
},
pleStatus: {
immediate: true,
handler(newVal, oldVal) {
const {
before,
after,
fulldate,
which
} = newVal
this.tempRange.before = before
this.tempRange.after = after
setTimeout(() => {
if (fulldate) {
this.cale.setHoverMultiple(fulldate)
if (before && after) {
this.cale.lastHover = true
if (this.rangeWithinMonth(after, before)) return
this.setDate(before)
} else {
this.cale.setMultiple(fulldate)
this.setDate(this.nowDate.fullDate)
this.calendar.fullDate = ''
this.cale.lastHover = false
}
} else {
this.cale.setDefaultMultiple(before, after)
if (which === 'left') {
this.setDate(before)
this.weeks = this.cale.weeks
} else {
this.setDate(after)
this.weeks = this.cale.weeks
}
this.cale.lastHover = true
}
}, 16)
}
}
},
computed: {
reactStartTime() {
const activeDate = this.range ? this.tempRange.before : this.calendar.fullDate
const res = activeDate === this.startDate ? this.selectableTimes.start : ''
return res
},
reactEndTime() {
const activeDate = this.range ? this.tempRange.after : this.calendar.fullDate
const res = activeDate === this.endDate ? this.selectableTimes.end : ''
return res
},
/**
* for i18n
*/
selectDateText() {
return t("uni-datetime-picker.selectDate")
},
startDateText() {
return this.startPlaceholder || t("uni-datetime-picker.startDate")
},
endDateText() {
return this.endPlaceholder || t("uni-datetime-picker.endDate")
},
okText() {
return t("uni-datetime-picker.ok")
},
monText() {
return t("uni-calender.MON")
},
TUEText() {
return t("uni-calender.TUE")
},
WEDText() {
return t("uni-calender.WED")
},
THUText() {
return t("uni-calender.THU")
},
FRIText() {
return t("uni-calender.FRI")
},
SATText() {
return t("uni-calender.SAT")
},
SUNText() {
return t("uni-calender.SUN")
},
},
created() {
// 获取日历方法实例
this.cale = new Calendar({
// date: new Date(),
selected: this.selected,
startDate: this.startDate,
endDate: this.endDate,
range: this.range,
// multipleStatus: this.pleStatus
})
// 选中某一天
// this.cale.setDate(this.date)
this.init(this.date)
// this.setDay
},
methods: {
leaveCale() {
this.firstEnter = true
},
handleMouse(weeks) {
if (weeks.disable) return
if (this.cale.lastHover) return
let {
before,
after
} = this.cale.multipleStatus
if (!before) return
this.calendar = weeks
// 设置范围选
this.cale.setHoverMultiple(this.calendar.fullDate)
this.weeks = this.cale.weeks
// hover时进入一个日历更新另一个
if (this.firstEnter) {
this.$emit('firstEnterCale', this.cale.multipleStatus)
this.firstEnter = false
}
},
rangeWithinMonth(A, B) {
const [yearA, monthA] = A.split('-')
const [yearB, monthB] = B.split('-')
return yearA === yearB && monthA === monthB
},
// 取消穿透
clean() {
this.close()
},
clearCalender() {
if (this.range) {
this.timeRange.startTime = ''
this.timeRange.endTime = ''
this.tempRange.before = ''
this.tempRange.after = ''
this.cale.multipleStatus.before = ''
this.cale.multipleStatus.after = ''
this.cale.multipleStatus.data = []
this.cale.lastHover = false
} else {
this.time = ''
this.tempSingleDate = ''
}
this.calendar.fullDate = ''
this.setDate()
},
bindDateChange(e) {
const value = e.detail.value + '-1'
this.init(value)
},
/**
* 初始化日期显示
* @param {Object} date
*/
init(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.calendar = this.cale.getInfo(date)
},
// choiceDate(weeks) {
// if (weeks.disable) return
// this.calendar = weeks
// // 设置多选
// this.cale.setMultiple(this.calendar.fullDate, true)
// this.weeks = this.cale.weeks
// this.tempSingleDate = this.calendar.fullDate
// this.tempRange.before = this.cale.multipleStatus.before
// this.tempRange.after = this.cale.multipleStatus.after
// this.change()
// },
/**
* 打开日历弹窗
*/
open() {
// 弹窗模式并且清理数据
if (this.clearDate && !this.insert) {
this.cale.cleanMultipleStatus()
// this.cale.setDate(this.date)
this.init(this.date)
}
this.show = true
this.$nextTick(() => {
setTimeout(() => {
this.aniMaskShow = true
}, 50)
})
},
/**
* 关闭日历弹窗
*/
close() {
this.aniMaskShow = false
this.$nextTick(() => {
setTimeout(() => {
this.show = false
this.$emit('close')
}, 300)
})
},
/**
* 确认按钮
*/
confirm() {
this.setEmit('confirm')
this.close()
},
/**
* 变化触发
*/
change() {
if (!this.insert) return
this.setEmit('change')
},
/**
* 选择月份触发
*/
monthSwitch() {
let {
year,
month
} = this.nowDate
this.$emit('monthSwitch', {
year,
month: Number(month)
})
},
/**
* 派发事件
* @param {Object} name
*/
setEmit(name) {
let {
year,
month,
date,
fullDate,
lunar,
extraInfo
} = this.calendar
this.$emit(name, {
range: this.cale.multipleStatus,
year,
month,
date,
time: this.time,
timeRange: this.timeRange,
fulldate: fullDate,
lunar,
extraInfo: extraInfo || {}
})
},
/**
* 选择天触发
* @param {Object} weeks
*/
choiceDate(weeks) {
if (weeks.disable) return
this.calendar = weeks
this.calendar.userChecked = true
// 设置多选
this.cale.setMultiple(this.calendar.fullDate, true)
this.weeks = this.cale.weeks
this.tempSingleDate = this.calendar.fullDate
this.tempRange.before = this.cale.multipleStatus.before
this.tempRange.after = this.cale.multipleStatus.after
this.change()
},
/**
* 回到今天
*/
backtoday() {
let date = this.cale.getDate(new Date()).fullDate
// this.cale.setDate(date)
this.init(date)
this.change()
},
/**
* 比较时间大小
*/
dateCompare(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
if (startDate <= endDate) {
return true
} else {
return false
}
},
/**
* 上个月
*/
pre() {
const preDate = this.cale.getDate(this.nowDate.fullDate, -1, 'month').fullDate
this.setDate(preDate)
this.monthSwitch()
},
/**
* 下个月
*/
next() {
const nextDate = this.cale.getDate(this.nowDate.fullDate, +1, 'month').fullDate
this.setDate(nextDate)
this.monthSwitch()
},
/**
* 设置日期
* @param {Object} date
*/
setDate(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.cale.getInfo(date)
}
}
}
</script>
<style lang="scss" scoped>
.uni-calendar {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
}
.uni-calendar__mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
transition-property: opacity;
transition-duration: 0.3s;
opacity: 0;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-calendar--mask-show {
opacity: 1
}
.uni-calendar--fixed {
position: fixed;
bottom: calc(var(--window-bottom));
left: 0;
right: 0;
transition-property: transform;
transition-duration: 0.3s;
transform: translateY(4.6rem);
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-calendar--ani-show {
transform: translateY(0);
}
.uni-calendar__content {
background-color: #fff;
}
.uni-calendar__content-mobile {
border-top-left-radius: 0.1rem;
border-top-right-radius: 0.1rem;
box-shadow: 0 0 0.05rem 0.03rem rgba(0, 0, 0, 0.1);
}
.uni-calendar__header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
height: 0.5rem;
}
.uni-calendar__header-mobile {
padding: 0.1rem;
padding-bottom: 0;
}
.uni-calendar--fixed-top {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
border-top-color: rgba(0, 0, 0, 0.4);
border-top-style: solid;
border-top-width: 0.01rem;
}
.uni-calendar--fixed-width {
width: 0.5rem;
}
.uni-calendar__backtoday {
position: absolute;
right: 0;
top: 0.125rem;
padding: 0 0.05rem;
padding-left: 0.1rem;
height: 0.25rem;
line-height: 0.25rem;
font-size: 0.2rem;
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
color: #fff;
background-color: #f1f1f1;
}
.uni-calendar__header-text {
text-align: center;
width: 1rem;
font-size: 0.15rem;
color: #666;
}
.uni-calendar__button-text {
text-align: center;
width: 1rem;
font-size: 0.14rem;
color: $primary-color;
/* #ifndef APP-NVUE */
letter-spacing: 0.03rem;
/* #endif */
}
.uni-calendar__header-btn-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
width: 0.5rem;
height: 0.5rem;
}
.uni-calendar__header-btn {
width: 0.09rem;
height: 0.09rem;
border-left-color: #808080;
border-left-style: solid;
border-left-width: 0.01rem;
border-top-color: #555555;
border-top-style: solid;
border-top-width: 0.01rem;
}
.uni-calendar--left {
transform: rotate(-45deg);
}
.uni-calendar--right {
transform: rotate(135deg);
}
.uni-calendar__weeks {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-calendar__weeks-item {
flex: 1;
}
.uni-calendar__weeks-day {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
height: 0.4rem;
border-bottom-color: #F5F5F5;
border-bottom-style: solid;
border-bottom-width: 0.01rem;
}
.uni-calendar__weeks-day-text {
font-size: 0.2rem;
color: #B2B2B2;
}
.uni-calendar__box {
position: relative;
// padding: 0 0.1rem;
padding-bottom: 0.07rem;
}
.uni-calendar__box-bg {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.uni-calendar__box-bg-text {
font-size: 2rem;
font-weight: bold;
color: #999;
opacity: 0.1;
text-align: center;
/* #ifndef APP-NVUE */
line-height: 1;
/* #endif */
}
.uni-date-changed {
padding: 0 0.1rem;
// line-height: 0.5rem;
text-align: center;
color: #333;
border-top-color: #DCDCDC;
;
border-top-style: solid;
border-top-width: 0.01rem;
flex: 1;
}
.uni-date-btn--ok {
padding: 0.2rem 0.15rem;
}
.uni-date-changed--time-start {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
}
.uni-date-changed--time-end {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
}
.uni-date-changed--time-date {
color: #999;
line-height: 0.5rem;
margin-right: 0.05rem;
// opacity: 0.6;
}
.time-picker-style {
// width: 62px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center
}
.mr-10 {
margin-right: 0.1rem;
}
.dialog-close {
position: absolute;
top: 0;
right: 0;
bottom: 0;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
padding: 0 0.25rem;
margin-top: 0.1rem;
}
.dialog-close-plus {
width: 0.16rem;
height: 0.02rem;
background-color: #737987;
border-radius: 0.02rem;
transform: rotate(45deg);
}
.dialog-close-rotate {
position: absolute;
transform: rotate(-45deg);
}
.uni-datetime-picker--btn {
border-radius: 1rem;
height: 0.4rem;
line-height: 0.4rem;
background-color: $primary-color;
color: #fff;
font-size: 0.16rem;
letter-spacing: 0.05rem;
}
/* #ifndef APP-NVUE */
.uni-datetime-picker--btn:active {
opacity: 0.7;
}
/* #endif */
</style>

View File

@@ -0,0 +1,19 @@
{
"uni-datetime-picker.selectDate": "select date",
"uni-datetime-picker.selectTime": "select time",
"uni-datetime-picker.selectDateTime": "select datetime",
"uni-datetime-picker.startDate": "start date",
"uni-datetime-picker.endDate": "end date",
"uni-datetime-picker.startTime": "start time",
"uni-datetime-picker.endTime": "end time",
"uni-datetime-picker.ok": "ok",
"uni-datetime-picker.clear": "clear",
"uni-datetime-picker.cancel": "cancel",
"uni-calender.MON": "MON",
"uni-calender.TUE": "TUE",
"uni-calender.WED": "WED",
"uni-calender.THU": "THU",
"uni-calender.FRI": "FRI",
"uni-calender.SAT": "SAT",
"uni-calender.SUN": "SUN"
}

View File

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

View File

@@ -0,0 +1,19 @@
{
"uni-datetime-picker.selectDate": "选择日期",
"uni-datetime-picker.selectTime": "选择时间",
"uni-datetime-picker.selectDateTime": "选择日期时间",
"uni-datetime-picker.startDate": "开始日期",
"uni-datetime-picker.endDate": "结束日期",
"uni-datetime-picker.startTime": "开始时间",
"uni-datetime-picker.endTime": "结束时间",
"uni-datetime-picker.ok": "确定",
"uni-datetime-picker.clear": "清除",
"uni-datetime-picker.cancel": "取消",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六"
}

View File

@@ -0,0 +1,19 @@
{
"uni-datetime-picker.selectDate": "選擇日期",
"uni-datetime-picker.selectTime": "選擇時間",
"uni-datetime-picker.selectDateTime": "選擇日期時間",
"uni-datetime-picker.startDate": "開始日期",
"uni-datetime-picker.endDate": "結束日期",
"uni-datetime-picker.startTime": "開始时间",
"uni-datetime-picker.endTime": "結束时间",
"uni-datetime-picker.ok": "確定",
"uni-datetime-picker.clear": "清除",
"uni-datetime-picker.cancel": "取消",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六"
}

View File

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

View File

@@ -0,0 +1,921 @@
<template>
<view class="uni-datetime-picker">
<view @click="initTimePicker">
<slot>
<view class="uni-datetime-picker-timebox-pointer" :class="{'uni-datetime-picker-disabled': disabled, 'uni-datetime-picker-timebox': border}">
<text class="uni-datetime-picker-text">{{time}}</text>
<view v-if="!time" class="uni-datetime-picker-time">
<text class="uni-datetime-picker-text">{{selectTimeText}}</text>
</view>
</view>
</slot>
</view>
<view v-if="visible" id="mask" class="uni-datetime-picker-mask" @click="tiggerTimePicker"></view>
<view v-if="visible" class="uni-datetime-picker-popup" :class="[dateShow && timeShow ? '' : 'fix-nvue-height']" :style="fixNvueBug">
<view class="uni-title">
<text class="uni-datetime-picker-text">{{selectTimeText}}</text>
</view>
<view v-if="dateShow" class="uni-datetime-picker__container-box">
<picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="ymd" @change="bindDateChange">
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in years" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in months" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in days" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
</picker-view>
<!-- 兼容 nvue 不支持伪类 -->
<text class="uni-datetime-picker-sign sign-left">-</text>
<text class="uni-datetime-picker-sign sign-right">-</text>
</view>
<view v-if="timeShow" class="uni-datetime-picker__container-box">
<picker-view class="uni-datetime-picker-view" :class="[hideSecond ? 'time-hide-second' : '']" :indicator-style="indicatorStyle" :value="hms" @change="bindTimeChange">
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in hours" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in minutes" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
<picker-view-column v-if="!hideSecond">
<view class="uni-datetime-picker-item" v-for="(item,index) in seconds" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
</picker-view>
<!-- 兼容 nvue 不支持伪类 -->
<text class="uni-datetime-picker-sign" :class="[hideSecond ? 'sign-center' : 'sign-left']">:</text>
<text v-if="!hideSecond" class="uni-datetime-picker-sign sign-right">:</text>
</view>
<view class="uni-datetime-picker-btn">
<view @click="clearTime">
<text class="uni-datetime-picker-btn-text">{{clearText}}</text>
</view>
<view class="uni-datetime-picker-btn-group">
<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">
<text class="uni-datetime-picker-btn-text">{{cancelText}}</text>
</view>
<view @click="setTime">
<text class="uni-datetime-picker-btn-text">{{okText}}</text>
</view>
</view>
</view>
</view>
<!-- #ifdef H5 -->
<!-- <keypress v-if="visible" @esc="tiggerTimePicker" @enter="setTime" /> -->
<!-- #endif -->
</view>
</template>
<script>
// #ifdef H5
import keypress from './keypress'
// #endif
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const { t } = initVueI18n(messages)
/**
* DatetimePicker 时间选择器
* @description 可以同时选择日期和时间的选择器
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
* @property {String} type = [datetime | date | time] 显示模式
* @property {Boolean} multiple = [true|false] 是否多选
* @property {String|Number} value 默认值
* @property {String|Number} start 起始日期或时间
* @property {String|Number} end 起始日期或时间
* @property {String} return-type = [timestamp | string]
* @event {Function} change 选中发生变化触发
*/
export default {
name: 'UniDatetimePicker',
components: {
// #ifdef H5
keypress
// #endif
},
data() {
return {
indicatorStyle: `height: 0.5rem;`,
visible: false,
fixNvueBug: {},
dateShow: true,
timeShow: true,
title: '日期和时间',
// 输入框当前时间
time: '',
// 当前的年月日时分秒
year: 1920,
month: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
// 起始时间
startYear: 1920,
startMonth: 1,
startDay: 1,
startHour: 0,
startMinute: 0,
startSecond: 0,
// 结束时间
endYear: 2120,
endMonth: 12,
endDay: 31,
endHour: 23,
endMinute: 59,
endSecond: 59,
}
},
props: {
type: {
type: String,
default: 'datetime'
},
value: {
type: [String, Number],
default: ''
},
modelValue: {
type: [String, Number],
default: ''
},
start: {
type: [Number, String],
default: ''
},
end: {
type: [Number, String],
default: ''
},
returnType: {
type: String,
default: 'string'
},
disabled: {
type: [Boolean, String],
default: false
},
border: {
type: [Boolean, String],
default: true
},
hideSecond: {
type: [Boolean, String],
default: false
}
},
watch: {
value: {
handler(newVal, oldVal) {
if (newVal) {
this.parseValue(this.fixIosDateFormat(newVal)) //兼容 iOS、safari 日期格式
this.initTime(false)
} else {
this.time = ''
this.parseValue(Date.now())
}
},
immediate: true
},
type: {
handler(newValue) {
if (newValue === 'date') {
this.dateShow = true
this.timeShow = false
this.title = '日期'
} else if (newValue === 'time') {
this.dateShow = false
this.timeShow = true
this.title = '时间'
} else {
this.dateShow = true
this.timeShow = true
this.title = '日期和时间'
}
},
immediate: true
},
start: {
handler(newVal) {
this.parseDatetimeRange(this.fixIosDateFormat(newVal), 'start') //兼容 iOS、safari 日期格式
},
immediate: true
},
end: {
handler(newVal) {
this.parseDatetimeRange(this.fixIosDateFormat(newVal), 'end') //兼容 iOS、safari 日期格式
},
immediate: true
},
// 月、日、时、分、秒可选范围变化后,检查当前值是否在范围内,不在则当前值重置为可选范围第一项
months(newVal) {
this.checkValue('month', this.month, newVal)
},
days(newVal) {
this.checkValue('day', this.day, newVal)
},
hours(newVal) {
this.checkValue('hour', this.hour, newVal)
},
minutes(newVal) {
this.checkValue('minute', this.minute, newVal)
},
seconds(newVal) {
this.checkValue('second', this.second, newVal)
}
},
computed: {
// 当前年、月、日、时、分、秒选择范围
years() {
return this.getCurrentRange('year')
},
months() {
return this.getCurrentRange('month')
},
days() {
return this.getCurrentRange('day')
},
hours() {
return this.getCurrentRange('hour')
},
minutes() {
return this.getCurrentRange('minute')
},
seconds() {
return this.getCurrentRange('second')
},
// picker 当前值数组
ymd() {
return [this.year - this.minYear, this.month - this.minMonth, this.day - this.minDay]
},
hms() {
return [this.hour - this.minHour, this.minute - this.minMinute, this.second - this.minSecond]
},
// 当前 date 是 start
currentDateIsStart() {
return this.year === this.startYear && this.month === this.startMonth && this.day === this.startDay
},
// 当前 date 是 end
currentDateIsEnd() {
return this.year === this.endYear && this.month === this.endMonth && this.day === this.endDay
},
// 当前年、月、日、时、分、秒的最小值和最大值
minYear() {
return this.startYear
},
maxYear() {
return this.endYear
},
minMonth() {
if (this.year === this.startYear) {
return this.startMonth
} else {
return 1
}
},
maxMonth() {
if (this.year === this.endYear) {
return this.endMonth
} else {
return 12
}
},
minDay() {
if (this.year === this.startYear && this.month === this.startMonth) {
return this.startDay
} else {
return 1
}
},
maxDay() {
if (this.year === this.endYear && this.month === this.endMonth) {
return this.endDay
} else {
return this.daysInMonth(this.year, this.month)
}
},
minHour() {
if (this.type === 'datetime') {
if (this.currentDateIsStart) {
return this.startHour
} else {
return 0
}
}
if (this.type === 'time') {
return this.startHour
}
},
maxHour() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd) {
return this.endHour
} else {
return 23
}
}
if (this.type === 'time') {
return this.endHour
}
},
minMinute() {
if (this.type === 'datetime') {
if (this.currentDateIsStart && this.hour === this.startHour) {
return this.startMinute
} else {
return 0
}
}
if (this.type === 'time') {
if (this.hour === this.startHour) {
return this.startMinute
} else {
return 0
}
}
},
maxMinute() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd && this.hour === this.endHour) {
return this.endMinute
} else {
return 59
}
}
if (this.type === 'time') {
if (this.hour === this.endHour) {
return this.endMinute
} else {
return 59
}
}
},
minSecond() {
if (this.type === 'datetime') {
if (this.currentDateIsStart && this.hour === this.startHour && this.minute === this.startMinute) {
return this.startSecond
} else {
return 0
}
}
if (this.type === 'time') {
if (this.hour === this.startHour && this.minute === this.startMinute) {
return this.startSecond
} else {
return 0
}
}
},
maxSecond() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd && this.hour === this.endHour && this.minute === this.endMinute) {
return this.endSecond
} else {
return 59
}
}
if (this.type === 'time') {
if (this.hour === this.endHour && this.minute === this.endMinute) {
return this.endSecond
} else {
return 59
}
}
},
/**
* for i18n
*/
selectTimeText() {
return t("uni-datetime-picker.selectTime")
},
okText() {
return t("uni-datetime-picker.ok")
},
clearText() {
return t("uni-datetime-picker.clear")
},
cancelText() {
return t("uni-datetime-picker.cancel")
}
},
mounted() {
// #ifdef APP-NVUE
const res = uni.getSystemInfoSync();
this.fixNvueBug = {
top: res.windowHeight / 2,
left: res.windowWidth / 2
}
// #endif
},
methods: {
/**
* @param {Object} item
* 小于 10 在前面加个 0
*/
lessThanTen(item) {
return item < 10 ? '0' + item : item
},
/**
* 解析时分秒字符串例如00:00:00
* @param {String} timeString
*/
parseTimeType(timeString) {
if (timeString) {
let timeArr = timeString.split(':')
this.hour = Number(timeArr[0])
this.minute = Number(timeArr[1])
this.second = Number(timeArr[2])
}
},
/**
* 解析选择器初始值类型可以是字符串、时间戳例如2000-10-02、'08:30:00'、 1610695109000
* @param {String | Number} datetime
*/
initPickerValue(datetime) {
let defaultValue = null
if (datetime) {
defaultValue = this.compareValueWithStartAndEnd(datetime, this.start, this.end)
} else {
defaultValue = Date.now()
defaultValue = this.compareValueWithStartAndEnd(defaultValue, this.start, this.end)
}
this.parseValue(defaultValue)
},
/**
* 初始值规则:
* - 用户设置初始值 value
* - 设置了起始时间 start、终止时间 end并 start < value < end初始值为 value 否则初始值为 start
* - 只设置了起始时间 start并 start < value初始值为 value否则初始值为 start
* - 只设置了终止时间 end并 value < end初始值为 value否则初始值为 end
* - 无起始终止时间,则初始值为 value
* - 无初始值 value则初始值为当前本地时间 Date.now()
* @param {Object} value
* @param {Object} dateBase
*/
compareValueWithStartAndEnd(value, start, end) {
let winner = null
value = this.superTimeStamp(value)
start = this.superTimeStamp(start)
end = this.superTimeStamp(end)
if (start && end) {
if (value < start) {
winner = new Date(start)
} else if (value > end) {
winner = new Date(end)
} else {
winner = new Date(value)
}
} else if (start && !end) {
winner = start <= value ? new Date(value) : new Date(start)
} else if (!start && end) {
winner = value <= end ? new Date(value) : new Date(end)
} else {
winner = new Date(value)
}
return winner
},
/**
* 转换为可比较的时间戳,接受日期、时分秒、时间戳
* @param {Object} value
*/
superTimeStamp(value) {
let dateBase = ''
if (this.type === 'time' && value && typeof value === 'string') {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const day = now.getDate()
dateBase = year + '/' + month + '/' + day + ' '
}
if (Number(value) && typeof value !== NaN) {
value = parseInt(value)
dateBase = 0
}
return this.createTimeStamp(dateBase + value)
},
/**
* 解析默认值 value字符串、时间戳
* @param {Object} defaultTime
*/
parseValue(value) {
if (!value) {
return
}
if (this.type === 'time' && typeof value === "string") {
this.parseTimeType(value)
} else {
let defaultDate = null
defaultDate = new Date(value)
if (this.type !== 'time') {
this.year = defaultDate.getFullYear()
this.month = defaultDate.getMonth() + 1
this.day = defaultDate.getDate()
}
if (this.type !== 'date') {
this.hour = defaultDate.getHours()
this.minute = defaultDate.getMinutes()
this.second = defaultDate.getSeconds()
}
}
if (this.hideSecond) {
this.second = 0
}
},
/**
* 解析可选择时间范围 start、end年月日字符串、时间戳
* @param {Object} defaultTime
*/
parseDatetimeRange(point, pointType) {
// 时间为空,则重置为初始值
if (!point) {
if (pointType === 'start') {
this.startYear = 1920
this.startMonth = 1
this.startDay = 1
this.startHour = 0
this.startMinute = 0
this.startSecond = 0
}
if (pointType === 'end') {
this.endYear = 2120
this.endMonth = 12
this.endDay = 31
this.endHour = 23
this.endMinute = 59
this.endSecond = 59
}
return
}
if (this.type === 'time') {
const pointArr = point.split(':')
this[pointType + 'Hour'] = Number(pointArr[0])
this[pointType + 'Minute'] = Number(pointArr[1])
this[pointType + 'Second'] = Number(pointArr[2])
} else {
if (!point) {
pointType === 'start' ? this.startYear = this.year - 60 : this.endYear = this.year + 60
return
}
if (Number(point) && Number(point) !== NaN) {
point = parseInt(point)
}
// datetime 的 end 没有时分秒, 则不限制
const hasTime = /[0-9]:[0-9]/
if (this.type === 'datetime' && pointType === 'end' && typeof point === 'string' && !hasTime.test(
point)) {
point = point + ' 23:59:59'
}
const pointDate = new Date(point)
this[pointType + 'Year'] = pointDate.getFullYear()
this[pointType + 'Month'] = pointDate.getMonth() + 1
this[pointType + 'Day'] = pointDate.getDate()
if (this.type === 'datetime') {
this[pointType + 'Hour'] = pointDate.getHours()
this[pointType + 'Minute'] = pointDate.getMinutes()
this[pointType + 'Second'] = pointDate.getSeconds()
}
}
},
// 获取 年、月、日、时、分、秒 当前可选范围
getCurrentRange(value) {
const range = []
for (let i = this['min' + this.capitalize(value)]; i <= this['max' + this.capitalize(value)]; i++) {
range.push(i)
}
return range
},
// 字符串首字母大写
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
// 检查当前值是否在范围内,不在则当前值重置为可选范围第一项
checkValue(name, value, values) {
if (values.indexOf(value) === -1) {
this[name] = values[0]
}
},
// 每个月的实际天数
daysInMonth(year, month) { // Use 1 for January, 2 for February, etc.
return new Date(year, month, 0).getDate();
},
//兼容 iOS、safari 日期格式
fixIosDateFormat(value) {
if (typeof value === 'string') {
value = value.replace(/-/g, '/')
}
return value
},
/**
* 生成时间戳
* @param {Object} time
*/
createTimeStamp(time) {
if (!time) return
if (typeof time === "number") {
return time
} else {
time = time.replace(/-/g, '/')
if (this.type === 'date') {
time = time + ' ' + '00:00:00'
}
return Date.parse(time)
}
},
/**
* 生成日期或时间的字符串
*/
createDomSting() {
const yymmdd = this.year +
'-' +
this.lessThanTen(this.month) +
'-' +
this.lessThanTen(this.day)
let hhmmss = this.lessThanTen(this.hour) +
':' +
this.lessThanTen(this.minute)
if (!this.hideSecond) {
hhmmss = hhmmss + ':' + this.lessThanTen(this.second)
}
if (this.type === 'date') {
return yymmdd
} else if (this.type === 'time') {
return hhmmss
} else {
return yymmdd + ' ' + hhmmss
}
},
/**
* 初始化返回值,并抛出 change 事件
*/
initTime(emit = true) {
this.time = this.createDomSting()
if (!emit) return
if (this.returnType === 'timestamp' && this.type !== 'time') {
this.$emit('change', this.createTimeStamp(this.time))
this.$emit('input', this.createTimeStamp(this.time))
this.$emit('update:modelValue', this.createTimeStamp(this.time))
} else {
this.$emit('change', this.time)
this.$emit('input', this.time)
this.$emit('update:modelValue', this.time)
}
},
/**
* 用户选择日期或时间更新 data
* @param {Object} e
*/
bindDateChange(e) {
const val = e.detail.value
this.year = this.years[val[0]]
this.month = this.months[val[1]]
this.day = this.days[val[2]]
},
bindTimeChange(e) {
const val = e.detail.value
this.hour = this.hours[val[0]]
this.minute = this.minutes[val[1]]
this.second = this.seconds[val[2]]
},
/**
* 初始化弹出层
*/
initTimePicker() {
if (this.disabled) return
const value = this.fixIosDateFormat(this.value)
this.initPickerValue(value)
this.visible = !this.visible
},
/**
* 触发或关闭弹框
*/
tiggerTimePicker(e) {
this.visible = !this.visible
},
/**
* 用户点击“清空”按钮,清空当前值
*/
clearTime() {
this.time = ''
this.$emit('change', this.time)
this.$emit('input', this.time)
this.$emit('update:modelValue', this.time)
this.tiggerTimePicker()
},
/**
* 用户点击“确定”按钮
*/
setTime() {
this.initTime()
this.tiggerTimePicker()
}
}
}
</script>
<style>
.uni-datetime-picker {
/* #ifndef APP-NVUE */
/* width: 100%; */
/* #endif */
}
.uni-datetime-picker-view {
height: 1.3rem;
width: 2.7rem;
/* #ifndef APP-NVUE */
cursor: pointer;
/* #endif */
}
.uni-datetime-picker-item {
height: 0.5rem;
line-height: 0.5rem;
text-align: center;
font-size: 0.14rem;
}
.uni-datetime-picker-btn {
margin-top: 0.6rem;
/* #ifndef APP-NVUE */
display: flex;
cursor: pointer;
/* #endif */
flex-direction: row;
justify-content: space-between;
}
.uni-datetime-picker-btn-text {
font-size: 0.14rem;
color: $primary-color;
}
.uni-datetime-picker-btn-group {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-datetime-picker-cancel {
margin-right: 0.3rem;
}
.uni-datetime-picker-mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
transition-duration: 0.3s;
z-index: 998;
}
.uni-datetime-picker-popup {
border-radius: 0.08rem;
padding: 0.3rem;
width: 2.7rem;
/* #ifdef APP-NVUE */
height: 5rem;
/* #endif */
/* #ifdef APP-NVUE */
width: 3.3rem;
/* #endif */
background-color: #fff;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition-duration: 0.3s;
z-index: 999;
}
.fix-nvue-height {
/* #ifdef APP-NVUE */
height: 3.3rem;
/* #endif */
}
.uni-datetime-picker-time {
color: grey;
}
.uni-datetime-picker-column {
height: 0.5rem;
}
.uni-datetime-picker-timebox {
border: 0.01rem solid #E5E5E5;
border-radius: 0.05rem;
padding: 0.07rem 0.1rem;
/* #ifndef APP-NVUE */
box-sizing: border-box;
cursor: pointer;
/* #endif */
}
.uni-datetime-picker-timebox-pointer {
/* #ifndef APP-NVUE */
cursor: pointer;
/* #endif */
}
.uni-datetime-picker-disabled {
opacity: 0.4;
/* #ifdef H5 */
cursor: not-allowed !important;
/* #endif */
}
.uni-datetime-picker-text {
font-size: 0.14rem;
}
.uni-datetime-picker-sign {
position: absolute;
top: 0.53rem;
/* 减掉 0.1rem 的元素高度兼容nvue */
color: #999;
/* #ifdef APP-NVUE */
font-size: 0.16rem;
/* #endif */
}
.sign-left {
left: 0.86rem;
}
.sign-right {
right: 0.86rem;
}
.sign-center {
left: 1.35rem;
}
.uni-datetime-picker__container-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-top: 0.4rem;
}
.time-hide-second {
width: 1.8rem;
}
</style>

View File

@@ -0,0 +1,973 @@
<template>
<view class="uni-date">
<view class="uni-date-editor" @click="show">
<slot>
<view class="uni-date-editor--x" :class="{'uni-date-editor--x__disabled': disabled,'uni-date-x--border': border}">
<view v-if="!isRange" class="uni-date-x uni-date-single">
<uni-icons type="calendar" color="#e1e1e1" size="22"></uni-icons>
<input class="uni-date__x-input" type="text" v-model="singleVal" :placeholder="singlePlaceholderText" :disabled="inputDisabled" />
</view>
<view v-else class="uni-date-x uni-date-range">
<uni-icons type="calendar" color="#e1e1e1" size="22"></uni-icons>
<input class="uni-date__x-input t-c" type="text" v-model="range.startDate" :placeholder="startPlaceholderText" :disabled="inputDisabled" />
<slot>
<view>{{rangeSeparator}}</view>
</slot>
<input class="uni-date__x-input t-c" type="text" v-model="range.endDate" :placeholder="endPlaceholderText" :disabled="inputDisabled" />
</view>
<view v-if="showClearIcon" class="uni-date__icon-clear" @click.stop="clear">
<uni-icons type="clear" color="#e1e1e1" size="18"></uni-icons>
</view>
</view>
</slot>
</view>
<view v-show="popup" class="uni-date-mask" @click="close"></view>
<view v-if="!isPhone" ref="datePicker" v-show="popup" class="uni-date-picker__container">
<view v-if="!isRange" class="uni-date-single--x" :style="popover">
<view class="uni-popper__arrow"></view>
<view v-if="hasTime" class="uni-date-changed popup-x-header">
<input class="uni-date__input t-c" type="text" v-model="tempSingleDate" :placeholder="selectDateText" />
<time-picker type="time" v-model="time" :border="false" :disabled="!tempSingleDate" :start="reactStartTime" :end="reactEndTime" :hideSecond="hideSecond" style="width: 100%;">
<input class="uni-date__input t-c" type="text" v-model="time" :placeholder="selectTimeText" :disabled="!tempSingleDate" />
</time-picker>
</view>
<calendar ref="pcSingle" :showMonth="false" :start-date="caleRange.startDate" :end-date="caleRange.endDate" :date="defSingleDate" @change="singleChange" style="padding: 0 0.08rem;" />
<view v-if="hasTime" class="popup-x-footer">
<!-- <text>此刻</text> -->
<text class="confirm" @click="confirmSingleChange">{{okText}}</text>
</view>
<view class="uni-date-popper__arrow"></view>
</view>
<view v-else class="uni-date-range--x" :style="popover">
<view class="uni-popper__arrow"></view>
<view v-if="hasTime" class="popup-x-header uni-date-changed">
<view class="popup-x-header--datetime">
<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.startDate" :placeholder="startDateText" />
<time-picker type="time" v-model="tempRange.startTime" :start="reactStartTime" :border="false" :disabled="!tempRange.startDate" :hideSecond="hideSecond">
<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.startTime" :placeholder="startTimeText" :disabled="!tempRange.startDate" />
</time-picker>
</view>
<uni-icons type="arrowthinright" color="#999" style="line-height: 0.4rem;"></uni-icons>
<view class="popup-x-header--datetime">
<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.endDate" :placeholder="endDateText" />
<time-picker type="time" v-model="tempRange.endTime" :end="reactEndTime" :border="false" :disabled="!tempRange.endDate" :hideSecond="hideSecond">
<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.endTime" :placeholder="endTimeText" :disabled="!tempRange.endDate" />
</time-picker>
</view>
</view>
<view class="popup-x-body">
<calendar ref="left" :showMonth="false"
:start-date="caleRange.startDate" :end-date="caleRange.endDate" :range="true"
@change="leftChange" :pleStatus="endMultipleStatus" @firstEnterCale="updateRightCale"
@monthSwitch="leftMonthSwitch" style="padding: 0 0.08rem;" />
<calendar ref="right" :showMonth="false"
:start-date="caleRange.startDate" :end-date="caleRange.endDate" :range="true"
@change="rightChange" :pleStatus="startMultipleStatus" @firstEnterCale="updateLeftCale"
@monthSwitch="rightMonthSwitch" style="padding: 0 0.08rem;border-left: 0.01rem solid #F1F1F1;" />
</view>
<view v-if="hasTime" class="popup-x-footer">
<text @click="clear">{{clearText}}</text>
<text class="confirm" @click="confirmRangeChange">{{okText}}</text>
</view>
</view>
</view>
<calendar v-show="isPhone" ref="mobile" :clearDate="false" :date="defSingleDate" :defTime="reactMobDefTime"
:start-date="caleRange.startDate" :end-date="caleRange.endDate" :selectableTimes="mobSelectableTime"
:pleStatus="endMultipleStatus" :showMonth="false" :range="isRange" :typeHasTime="hasTime" :insert="false"
:hideSecond="hideSecond" @confirm="mobileChange" />
</view>
</template>
<script>
/**
* DatetimePicker 时间选择器
* @description 同时支持 PC 和移动端使用日历选择日期和日期范围
* @tutorial https://ext.dcloud.net.cn/plugin?id=3962
* @property {String} type 选择器类型
* @property {String|Number|Array|Date} value 绑定值
* @property {String} placeholder 单选择时的占位内容
* @property {String} start 起始时间
* @property {String} end 终止时间
* @property {String} start-placeholder 范围选择时开始日期的占位内容
* @property {String} end-placeholder 范围选择时结束日期的占位内容
* @property {String} range-separator 选择范围时的分隔符
* @property {Boolean} border = [true|false] 是否有边框
* @property {Boolean} disabled = [true|false] 是否禁用
* @property {Boolean} clearIcon = [true|false] 是否显示清除按钮仅PC端适用
* @event {Function} change 确定日期时触发的事件
* @event {Function} show 打开弹出层
* @event {Function} close 关闭弹出层
* @event {Function} clear 清除上次选中的状态和值
**/
import calendar from './calendar.vue'
import timePicker from './time-picker.vue'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const {
t
} = initVueI18n(messages)
export default {
name: 'UniDatetimePicker',
components: {
calendar,
timePicker
},
data() {
return {
isRange: false,
hasTime: false,
mobileRange: false,
// 单选
singleVal: '',
tempSingleDate: '',
defSingleDate: '',
time: '',
// 范围选
caleRange: {
startDate: '',
startTime: '',
endDate: '',
endTime: ''
},
range: {
startDate: '',
// startTime: '',
endDate: '',
// endTime: ''
},
tempRange: {
startDate: '',
startTime: '',
endDate: '',
endTime: ''
},
// 左右日历同步数据
startMultipleStatus: {
before: '',
after: '',
data: [],
fulldate: ''
},
endMultipleStatus: {
before: '',
after: '',
data: [],
fulldate: ''
},
visible: false,
popup: false,
popover: null,
isEmitValue: false,
isPhone: false,
isFirstShow: true,
}
},
props: {
inputDisabled: {
type: [Boolean],
default: true
},
type: {
type: String,
default: 'datetime'
},
value: {
type: [String, Number, Array, Date],
default: ''
},
modelValue: {
type: [String, Number, Array, Date],
default: ''
},
start: {
type: [Number, String],
default: ''
},
end: {
type: [Number, String],
default: ''
},
returnType: {
type: String,
default: 'string'
},
placeholder: {
type: String,
default: ''
},
startPlaceholder: {
type: String,
default: ''
},
endPlaceholder: {
type: String,
default: ''
},
rangeSeparator: {
type: String,
default: '-'
},
border: {
type: [Boolean],
default: true
},
disabled: {
type: [Boolean],
default: false
},
clearIcon: {
type: [Boolean],
default: true
},
hideSecond: {
type: [Boolean],
default: false
}
},
watch: {
type: {
immediate: true,
handler(newVal, oldVal) {
if (newVal.indexOf('time') !== -1) {
this.hasTime = true
} else {
this.hasTime = false
}
if (newVal.indexOf('range') !== -1) {
this.isRange = true
} else {
this.isRange = false
}
}
},
value: {
immediate: true,
handler(newVal, oldVal) {
if (this.isEmitValue) {
this.isEmitValue = false
return
}
this.initPicker(newVal)
}
},
start: {
immediate: true,
handler(newVal, oldVal) {
if (!newVal) return
const {
defDate,
defTime
} = this.parseDate(newVal)
this.caleRange.startDate = defDate
if (this.hasTime) {
this.caleRange.startTime = defTime
}
}
},
end: {
immediate: true,
handler(newVal, oldVal) {
if (!newVal) return
const {
defDate,
defTime
} = this.parseDate(newVal)
this.caleRange.endDate = defDate
if (this.hasTime) {
this.caleRange.endTime = defTime
}
}
},
},
computed: {
reactStartTime() {
const activeDate = this.isRange ? this.tempRange.startDate : this.tempSingleDate
const res = activeDate === this.caleRange.startDate ? this.caleRange.startTime : ''
return res
},
reactEndTime() {
const activeDate = this.isRange ? this.tempRange.endDate : this.tempSingleDate
const res = activeDate === this.caleRange.endDate ? this.caleRange.endTime : ''
return res
},
reactMobDefTime() {
const times = {
start: this.tempRange.startTime,
end: this.tempRange.endTime
}
return this.isRange ? times : this.time
},
mobSelectableTime() {
return {
start: this.caleRange.startTime,
end: this.caleRange.endTime
}
},
datePopupWidth() {
// todo
return this.isRange ? 653 : 301
},
/**
* for i18n
*/
singlePlaceholderText() {
return this.placeholder || (this.type === 'date' ? this.selectDateText : t(
"uni-datetime-picker.selectDateTime"))
},
startPlaceholderText() {
return this.startPlaceholder || this.startDateText
},
endPlaceholderText() {
return this.endPlaceholder || this.endDateText
},
selectDateText() {
return t("uni-datetime-picker.selectDate")
},
selectTimeText() {
return t("uni-datetime-picker.selectTime")
},
startDateText() {
return this.startPlaceholder || t("uni-datetime-picker.startDate")
},
startTimeText() {
return t("uni-datetime-picker.startTime")
},
endDateText() {
return this.endPlaceholder || t("uni-datetime-picker.endDate")
},
endTimeText() {
return t("uni-datetime-picker.endTime")
},
okText() {
return t("uni-datetime-picker.ok")
},
clearText() {
return t("uni-datetime-picker.clear")
},
showClearIcon() {
const { clearIcon, disabled, singleVal, range } = this
const bool = clearIcon && !disabled && (singleVal || (range.startDate && range.endDate))
return bool
}
},
created() {
this.form = this.getForm('uniForms')
this.formItem = this.getForm('uniFormsItem')
// if (this.formItem) {
// if (this.formItem.name) {
// this.rename = this.formItem.name
// this.form.inputChildrens.push(this)
// }
// }
},
mounted() {
this.platform()
},
methods: {
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
initPicker(newVal) {
if (!newVal || Array.isArray(newVal) && !newVal.length) {
this.$nextTick(() => {
this.clear(false)
})
return
}
if (!Array.isArray(newVal) && !this.isRange) {
const {
defDate,
defTime
} = this.parseDate(newVal)
this.singleVal = defDate
this.tempSingleDate = defDate
this.defSingleDate = defDate
if (this.hasTime) {
this.singleVal = defDate + ' ' + defTime
this.time = defTime
}
} else {
const [before, after] = newVal
if (!before && !after) return
const defBefore = this.parseDate(before)
const defAfter = this.parseDate(after)
const startDate = defBefore.defDate
const endDate = defAfter.defDate
this.range.startDate = this.tempRange.startDate = startDate
this.range.endDate = this.tempRange.endDate = endDate
if (this.hasTime) {
this.range.startDate = defBefore.defDate + ' ' + defBefore.defTime
this.range.endDate = defAfter.defDate + ' ' + defAfter.defTime
this.tempRange.startTime = defBefore.defTime
this.tempRange.endTime = defAfter.defTime
}
const defaultRange = {
before: defBefore.defDate,
after: defAfter.defDate
}
this.startMultipleStatus = Object.assign({}, this.startMultipleStatus, defaultRange, {
which: 'right'
})
this.endMultipleStatus = Object.assign({}, this.endMultipleStatus, defaultRange, {
which: 'left'
})
}
},
updateLeftCale(e) {
const left = this.$refs.left
// 设置范围选
left.cale.setHoverMultiple(e.after)
left.setDate(this.$refs.left.nowDate.fullDate)
},
updateRightCale(e) {
const right = this.$refs.right
// 设置范围选
right.cale.setHoverMultiple(e.after)
right.setDate(this.$refs.right.nowDate.fullDate)
},
platform() {
const systemInfo = uni.getSystemInfoSync()
this.isPhone = systemInfo.windowWidth <= 500
this.windowWidth = systemInfo.windowWidth
},
show(event) {
if (this.disabled) {
return
}
this.platform()
if (this.isPhone) {
this.$refs.mobile.open()
return
}
this.popover = {
top: '0.1rem'
}
const dateEditor = uni.createSelectorQuery().in(this).select(".uni-date-editor")
dateEditor.boundingClientRect(rect => {
if (this.windowWidth - rect.left < this.datePopupWidth) {
this.popover.right = 0
}
}).exec()
setTimeout(() => {
this.popup = !this.popup
if (!this.isPhone && this.isRange && this.isFirstShow) {
this.isFirstShow = false
const {
startDate,
endDate
} = this.range
if (startDate && endDate) {
if (this.diffDate(startDate, endDate) < 30) {
this.$refs.right.next()
}
} else {
this.$refs.right.next()
this.$refs.right.cale.lastHover = false
}
}
}, 50)
},
close() {
setTimeout(() => {
this.popup = false
this.$emit('maskClick', this.value)
}, 20)
},
setEmit(value) {
if (this.returnType === "timestamp" || this.returnType === "date") {
if (!Array.isArray(value)) {
if (!this.hasTime) {
value = value + ' ' + '00:00:00'
}
value = this.createTimestamp(value)
if (this.returnType === "date") {
value = new Date(value)
}
} else {
if (!this.hasTime) {
value[0] = value[0] + ' ' + '00:00:00'
value[1] = value[1] + ' ' + '00:00:00'
}
value[0] = this.createTimestamp(value[0])
value[1] = this.createTimestamp(value[1])
if (this.returnType === "date") {
value[0] = new Date(value[0])
value[1] = new Date(value[1])
}
}
}
this.formItem && this.formItem.setValue(value)
this.$emit('change', value)
this.$emit('input', value)
this.$emit('update:modelValue', value)
this.isEmitValue = true
},
createTimestamp(date) {
date = this.fixIosDateFormat(date)
return Date.parse(new Date(date))
},
singleChange(e) {
this.tempSingleDate = e.fulldate
if (this.hasTime) return
this.confirmSingleChange()
},
confirmSingleChange() {
if (!this.tempSingleDate) {
this.popup = false
return
}
if (this.hasTime) {
this.singleVal = this.tempSingleDate + ' ' + (this.time ? this.time : '00:00:00')
} else {
this.singleVal = this.tempSingleDate
}
this.setEmit(this.singleVal)
this.popup = false
},
leftChange(e) {
const {
before,
after
} = e.range
this.rangeChange(before, after)
const obj = {
before: e.range.before,
after: e.range.after,
data: e.range.data,
fulldate: e.fulldate
}
this.startMultipleStatus = Object.assign({}, this.startMultipleStatus, obj)
},
rightChange(e) {
const {
before,
after
} = e.range
this.rangeChange(before, after)
const obj = {
before: e.range.before,
after: e.range.after,
data: e.range.data,
fulldate: e.fulldate
}
this.endMultipleStatus = Object.assign({}, this.endMultipleStatus, obj)
},
mobileChange(e) {
if (this.isRange) {
const {
before,
after
} = e.range
this.handleStartAndEnd(before, after, true)
if (this.hasTime) {
const {
startTime,
endTime
} = e.timeRange
this.tempRange.startTime = startTime
this.tempRange.endTime = endTime
}
this.confirmRangeChange()
} else {
if (this.hasTime) {
this.singleVal = e.fulldate + ' ' + e.time
} else {
this.singleVal = e.fulldate
}
this.setEmit(this.singleVal)
}
this.$refs.mobile.close()
},
rangeChange(before, after) {
if (!(before && after)) return
this.handleStartAndEnd(before, after, true)
if (this.hasTime) return
this.confirmRangeChange()
},
confirmRangeChange() {
if (!this.tempRange.startDate && !this.tempRange.endDate) {
this.popup = false
return
}
let start, end
if (!this.hasTime) {
start = this.range.startDate = this.tempRange.startDate
end = this.range.endDate = this.tempRange.endDate
} else {
start = this.range.startDate = this.tempRange.startDate + ' ' +
(this.tempRange.startTime ? this.tempRange.startTime : '00:00:00')
end = this.range.endDate = this.tempRange.endDate + ' ' +
(this.tempRange.endTime ? this.tempRange.endTime : '00:00:00')
}
const displayRange = [start, end]
this.setEmit(displayRange)
this.popup = false
},
handleStartAndEnd(before, after, temp = false) {
if (!(before && after)) return
const type = temp ? 'tempRange' : 'range'
if (this.dateCompare(before, after)) {
this[type].startDate = before
this[type].endDate = after
} else {
this[type].startDate = after
this[type].endDate = before
}
},
/**
* 比较时间大小
*/
dateCompare(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
if (startDate <= endDate) {
return true
} else {
return false
}
},
/**
* 比较时间差
*/
diffDate(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
const diff = (endDate - startDate) / (24 * 60 * 60 * 1000)
return Math.abs(diff)
},
clear(needEmit = true) {
if (!this.isRange) {
this.singleVal = ''
this.tempSingleDate = ''
this.time = ''
if (this.isPhone) {
this.$refs.mobile && this.$refs.mobile.clearCalender()
} else {
this.$refs.pcSingle && this.$refs.pcSingle.clearCalender()
}
if (needEmit) {
this.formItem && this.formItem.setValue('')
this.$emit('change', '')
this.$emit('input', '')
this.$emit('update:modelValue', '')
}
} else {
this.range.startDate = ''
this.range.endDate = ''
this.tempRange.startDate = ''
this.tempRange.startTime = ''
this.tempRange.endDate = ''
this.tempRange.endTime = ''
if (this.isPhone) {
this.$refs.mobile && this.$refs.mobile.clearCalender()
} else {
this.$refs.left && this.$refs.left.clearCalender()
this.$refs.right && this.$refs.right.clearCalender()
this.$refs.right && this.$refs.right.next()
}
if (needEmit) {
this.formItem && this.formItem.setValue([])
this.$emit('change', [])
this.$emit('input', [])
this.$emit('update:modelValue', [])
}
}
},
parseDate(date) {
date = this.fixIosDateFormat(date)
const defVal = new Date(date)
const year = defVal.getFullYear()
const month = defVal.getMonth() + 1
const day = defVal.getDate()
const hour = defVal.getHours()
const minute = defVal.getMinutes()
const second = defVal.getSeconds()
const defDate = year + '-' + this.lessTen(month) + '-' + this.lessTen(day)
const defTime = this.lessTen(hour) + ':' + this.lessTen(minute) + (this.hideSecond ? '' : (':' + this
.lessTen(second)))
return {
defDate,
defTime
}
},
lessTen(item) {
return item < 10 ? '0' + item : item
},
//兼容 iOS、safari 日期格式
fixIosDateFormat(value) {
if (typeof value === 'string') {
value = value.replace(/-/g, '/')
}
return value
},
leftMonthSwitch(e) {
// console.log('leftMonthSwitch 返回:', e)
},
rightMonthSwitch(e) {
// console.log('rightMonthSwitch 返回:', e)
}
}
}
</script>
<style lang="scss">
.uni-date-x {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 0.1rem;
border-radius: 0.02rem;
background-color: #fff;
color: #666;
font-size: 0.14rem;
}
.uni-date-x--border {
box-sizing: border-box;
border-radius: 0.02rem;
border: 0.01rem solid #dcdfe6;
}
.uni-icons{
box-sizing: border-box;
}
.uni-date-editor--x {
position: relative;
}
.uni-date-editor--x .uni-date__icon-clear {
position: absolute;
top: 0;
right: 0;
display: inline-block;
box-sizing: border-box;
border: 0.09rem solid transparent;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-date__x-input {
padding: 0 0.08rem;
/* height: 0.4rem; */
width: 100%;
/* line-height: 0.4rem; */
font-size: 0.14rem;
}
.t-c {
text-align: center;
}
.uni-date__input {
height: 0.4rem;
width: 100%;
line-height: 0.4rem;
font-size: 0.14rem;
}
.uni-date-range__input {
text-align: center;
max-width: 1.42rem;
}
.uni-date-picker__container {
position: relative;
/* position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
box-sizing: border-box;
z-index: 996;
font-size: 0.14rem; */
}
.uni-date-mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0);
transition-duration: 0.3s;
z-index: 996;
}
.uni-date-single--x {
/* padding: 0 0.08rem; */
background-color: #fff;
position: absolute;
top: 0;
z-index: 999;
border: 0.01rem solid #EBEEF5;
box-shadow: 0 0.02rem 0.12rem 0 rgba(0, 0, 0, 0.1);
border-radius: 0.04rem;
}
.uni-date-range--x {
/* padding: 0 0.08rem; */
background-color: #fff;
position: absolute;
top: 0;
z-index: 999;
border: 0.01rem solid #EBEEF5;
box-shadow: 0 0.02rem 0.12rem 0 rgba(0, 0, 0, 0.1);
border-radius: 0.04rem;
}
.uni-date-editor--x__disabled {
opacity: 0.4;
cursor: default;
}
.uni-date-editor--logo {
width: 0.16rem;
height: 0.16rem;
vertical-align: middle;
}
/* 添加时间 */
.popup-x-header {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
/* justify-content: space-between; */
}
.popup-x-header--datetime {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex: 1;
}
.popup-x-body {
display: flex;
}
.popup-x-footer {
padding: 0 0.15rem;
border-top-color: #F1F1F1;
border-top-style: solid;
border-top-width: 0.01rem;
/* background-color: #fff; */
line-height: 0.4rem;
text-align: right;
color: #666;
}
.popup-x-footer text:hover {
color: $primary-color;
cursor: pointer;
opacity: 0.8;
}
.popup-x-footer .confirm {
margin-left: 0.2rem;
color: $primary-color;
}
.uni-date-changed {
/* background-color: #fff; */
text-align: center;
color: #333;
border-bottom-color: #F1F1F1;
border-bottom-style: solid;
border-bottom-width: 0.01rem;
/* padding: 0 50px; */
}
.uni-date-changed--time text {
/* padding: 0 20px; */
height: 0.5rem;
line-height: 0.5rem;
}
.uni-date-changed .uni-date-changed--time {
/* display: flex; */
flex: 1;
}
.uni-date-changed--time-date {
color: #333;
opacity: 0.6;
}
.mr-50 {
margin-right: 0.5rem;
}
/* picker 弹出层通用的指示小三角, todo扩展至上下左右方向定位 */
.uni-popper__arrow,
.uni-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 0.06rem;
}
.uni-popper__arrow {
filter: drop-shadow(0 0.02rem 0.12rem rgba(0, 0, 0, 0.03));
top: -0.06rem;
left: 10%;
margin-right: 0.03rem;
border-top-width: 0;
border-bottom-color: #EBEEF5;
}
.uni-popper__arrow::after {
content: " ";
top: 0.01rem;
margin-left: -0.06rem;
border-top-width: 0;
border-bottom-color: #fff;
}
</style>

View File

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

View File

@@ -0,0 +1,90 @@
<template>
<view class="dropdown">
<view class="dropdown-link" @click="open">
<slot name="dropdown-link"></slot>
</view>
<uni-transition key="1" name="mask" mode-class="fade" :styles="maskClass" :duration="300" :show="showTrans" @click="close" />
<uni-transition key="2" :mode-class="direction" class="dropdown-box" name="content" :style="dropdownClass" :duration="300" :show="showTrans">
<view class="dropdown-content" @click="close">
<slot name="dropdown"></slot>
</view>
</uni-transition>
</view>
</template>
<script>
export default {
name: "uniDropdown",
props: {
direction: {
type: String,
default: "slide-top",
},
},
data() {
return {
showTrans: false,
maskClass: {
position: "fixed",
zIndex: 1,
bottom: 0,
top: 0,
left: 0,
right: 0,
backgroundColor: "rgba(0, 0, 0, 0)",
},
dropdownClass: {},
};
},
methods: {
open() {
this.showTrans = true;
const query = uni.createSelectorQuery().in(this);
query.select(".dropdown-link").boundingClientRect((data) => {
switch (this.direction) {
case "slide-top":
this.dropdownClass = {
position: "absolute",
zIndex: 1,
top: "100%",
right: "0px",
};
break;
case "slide-bottom":
this.dropdownClass = {
position: "absolute",
zIndex: 1,
bottom: "100%",
left: "0px",
};
break;
}
}).exec();
},
close() {
this.showTrans = false;
},
},
};
</script>
<style lang="scss">
.dropdown-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: none;
z-index: 9999;
}
.dropdown {
position: relative;
}
.dropdown-box {
z-index: 999 !important;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
<template>
<!-- #ifdef APP-NVUE -->
<text :style="{ color: color, 'font-size': size + 'px' }" class="uni-icons" @click="_onClick">{{unicode}}</text>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<text :style="{ color: color, 'font-size': size + 'px' }" class="uni-icons" :class="['uniui-'+type,customPrefix,customPrefix?type:'']" @click="_onClick"></text>
<!-- #endif -->
</template>
<script>
import icons from './icons.js';
// #ifdef APP-NVUE
var domModule = weex.requireModule('dom');
import iconUrl from './uniicons.ttf'
domModule.addRule('fontFace', {
'fontFamily': "uniicons",
'src': "url('"+iconUrl+"')"
});
// #endif
/**
* Icons 图标
* @description 用于展示 icons 图标
* @tutorial https://ext.dcloud.net.cn/plugin?id=28
* @property {Number} size 图标大小
* @property {String} type 图标图案,参考示例
* @property {String} color 图标颜色
* @property {String} customPrefix 自定义图标
* @event {Function} click 点击 Icon 触发事件
*/
export default {
name: 'UniIcons',
emits:['click'],
props: {
type: {
type: String,
default: ''
},
color: {
type: String,
default: '#333333'
},
size: {
type: [Number, String],
default: 16
},
customPrefix:{
type: String,
default: ''
}
},
data() {
return {
icons: icons.glyphs
}
},
computed:{
unicode(){
let code = this.icons.find(v=>v.font_class === this.type)
if(code){
return unescape(`%u${code.unicode}`)
}
return ''
}
},
methods: {
_onClick() {
this.$emit('click')
}
}
}
</script>
<style lang="scss">
/* #ifndef APP-NVUE */
@import './uniicons.css';
@font-face {
font-family: uniicons;
src: url('./uniicons.ttf') format('truetype');
}
/* #endif */
.uni-icons {
font-family: uniicons;
text-decoration: none;
text-align: center;
}
</style>

View File

@@ -0,0 +1,663 @@
.uniui-color:before {
content: "\e6cf";
}
.uniui-wallet:before {
content: "\e6b1";
}
.uniui-settings-filled:before {
content: "\e6ce";
}
.uniui-auth-filled:before {
content: "\e6cc";
}
.uniui-shop-filled:before {
content: "\e6cd";
}
.uniui-staff-filled:before {
content: "\e6cb";
}
.uniui-vip-filled:before {
content: "\e6c6";
}
.uniui-plus-filled:before {
content: "\e6c7";
}
.uniui-folder-add-filled:before {
content: "\e6c8";
}
.uniui-color-filled:before {
content: "\e6c9";
}
.uniui-tune-filled:before {
content: "\e6ca";
}
.uniui-calendar-filled:before {
content: "\e6c0";
}
.uniui-notification-filled:before {
content: "\e6c1";
}
.uniui-wallet-filled:before {
content: "\e6c2";
}
.uniui-medal-filled:before {
content: "\e6c3";
}
.uniui-gift-filled:before {
content: "\e6c4";
}
.uniui-fire-filled:before {
content: "\e6c5";
}
.uniui-refreshempty:before {
content: "\e6bf";
}
.uniui-location-filled:before {
content: "\e6af";
}
.uniui-person-filled:before {
content: "\e69d";
}
.uniui-personadd-filled:before {
content: "\e698";
}
.uniui-back:before {
content: "\e6b9";
}
.uniui-forward:before {
content: "\e6ba";
}
.uniui-arrow-right:before {
content: "\e6bb";
}
.uniui-arrowthinright:before {
content: "\e6bb";
}
.uniui-arrow-left:before {
content: "\e6bc";
}
.uniui-arrowthinleft:before {
content: "\e6bc";
}
.uniui-arrow-up:before {
content: "\e6bd";
}
.uniui-arrowthinup:before {
content: "\e6bd";
}
.uniui-arrow-down:before {
content: "\e6be";
}
.uniui-arrowthindown:before {
content: "\e6be";
}
.uniui-bottom:before {
content: "\e6b8";
}
.uniui-arrowdown:before {
content: "\e6b8";
}
.uniui-right:before {
content: "\e6b5";
}
.uniui-arrowright:before {
content: "\e6b5";
}
.uniui-top:before {
content: "\e6b6";
}
.uniui-arrowup:before {
content: "\e6b6";
}
.uniui-left:before {
content: "\e6b7";
}
.uniui-arrowleft:before {
content: "\e6b7";
}
.uniui-eye:before {
content: "\e651";
}
.uniui-eye-filled:before {
content: "\e66a";
}
.uniui-eye-slash:before {
content: "\e6b3";
}
.uniui-eye-slash-filled:before {
content: "\e6b4";
}
.uniui-info-filled:before {
content: "\e649";
}
.uniui-reload:before {
content: "\e6b2";
}
.uniui-micoff-filled:before {
content: "\e6b0";
}
.uniui-map-pin-ellipse:before {
content: "\e6ac";
}
.uniui-map-pin:before {
content: "\e6ad";
}
.uniui-location:before {
content: "\e6ae";
}
.uniui-starhalf:before {
content: "\e683";
}
.uniui-star:before {
content: "\e688";
}
.uniui-star-filled:before {
content: "\e68f";
}
.uniui-calendar:before {
content: "\e6a0";
}
.uniui-fire:before {
content: "\e6a1";
}
.uniui-medal:before {
content: "\e6a2";
}
.uniui-font:before {
content: "\e6a3";
}
.uniui-gift:before {
content: "\e6a4";
}
.uniui-link:before {
content: "\e6a5";
}
.uniui-notification:before {
content: "\e6a6";
}
.uniui-staff:before {
content: "\e6a7";
}
.uniui-vip:before {
content: "\e6a8";
}
.uniui-folder-add:before {
content: "\e6a9";
}
.uniui-tune:before {
content: "\e6aa";
}
.uniui-auth:before {
content: "\e6ab";
}
.uniui-person:before {
content: "\e699";
}
.uniui-email-filled:before {
content: "\e69a";
}
.uniui-phone-filled:before {
content: "\e69b";
}
.uniui-phone:before {
content: "\e69c";
}
.uniui-email:before {
content: "\e69e";
}
.uniui-personadd:before {
content: "\e69f";
}
.uniui-chatboxes-filled:before {
content: "\e692";
}
.uniui-contact:before {
content: "\e693";
}
.uniui-chatbubble-filled:before {
content: "\e694";
}
.uniui-contact-filled:before {
content: "\e695";
}
.uniui-chatboxes:before {
content: "\e696";
}
.uniui-chatbubble:before {
content: "\e697";
}
.uniui-upload-filled:before {
content: "\e68e";
}
.uniui-upload:before {
content: "\e690";
}
.uniui-weixin:before {
content: "\e691";
}
.uniui-compose:before {
content: "\e67f";
}
.uniui-qq:before {
content: "\e680";
}
.uniui-download-filled:before {
content: "\e681";
}
.uniui-pyq:before {
content: "\e682";
}
.uniui-sound:before {
content: "\e684";
}
.uniui-trash-filled:before {
content: "\e685";
}
.uniui-sound-filled:before {
content: "\e686";
}
.uniui-trash:before {
content: "\e687";
}
.uniui-videocam-filled:before {
content: "\e689";
}
.uniui-spinner-cycle:before {
content: "\e68a";
}
.uniui-weibo:before {
content: "\e68b";
}
.uniui-videocam:before {
content: "\e68c";
}
.uniui-download:before {
content: "\e68d";
}
.uniui-help:before {
content: "\e679";
}
.uniui-navigate-filled:before {
content: "\e67a";
}
.uniui-plusempty:before {
content: "\e67b";
}
.uniui-smallcircle:before {
content: "\e67c";
}
.uniui-minus-filled:before {
content: "\e67d";
}
.uniui-micoff:before {
content: "\e67e";
}
.uniui-closeempty:before {
content: "\e66c";
}
.uniui-clear:before {
content: "\e66d";
}
.uniui-navigate:before {
content: "\e66e";
}
.uniui-minus:before {
content: "\e66f";
}
.uniui-image:before {
content: "\e670";
}
.uniui-mic:before {
content: "\e671";
}
.uniui-paperplane:before {
content: "\e672";
}
.uniui-close:before {
content: "\e673";
}
.uniui-help-filled:before {
content: "\e674";
}
.uniui-paperplane-filled:before {
content: "\e675";
}
.uniui-plus:before {
content: "\e676";
}
.uniui-mic-filled:before {
content: "\e677";
}
.uniui-image-filled:before {
content: "\e678";
}
.uniui-locked-filled:before {
content: "\e668";
}
.uniui-info:before {
content: "\e669";
}
.uniui-locked:before {
content: "\e66b";
}
.uniui-camera-filled:before {
content: "\e658";
}
.uniui-chat-filled:before {
content: "\e659";
}
.uniui-camera:before {
content: "\e65a";
}
.uniui-circle:before {
content: "\e65b";
}
.uniui-checkmarkempty:before {
content: "\e65c";
}
.uniui-chat:before {
content: "\e65d";
}
.uniui-circle-filled:before {
content: "\e65e";
}
.uniui-flag:before {
content: "\e65f";
}
.uniui-flag-filled:before {
content: "\e660";
}
.uniui-gear-filled:before {
content: "\e661";
}
.uniui-home:before {
content: "\e662";
}
.uniui-home-filled:before {
content: "\e663";
}
.uniui-gear:before {
content: "\e664";
}
.uniui-smallcircle-filled:before {
content: "\e665";
}
.uniui-map-filled:before {
content: "\e666";
}
.uniui-map:before {
content: "\e667";
}
.uniui-refresh-filled:before {
content: "\e656";
}
.uniui-refresh:before {
content: "\e657";
}
.uniui-cloud-upload:before {
content: "\e645";
}
.uniui-cloud-download-filled:before {
content: "\e646";
}
.uniui-cloud-download:before {
content: "\e647";
}
.uniui-cloud-upload-filled:before {
content: "\e648";
}
.uniui-redo:before {
content: "\e64a";
}
.uniui-images-filled:before {
content: "\e64b";
}
.uniui-undo-filled:before {
content: "\e64c";
}
.uniui-more:before {
content: "\e64d";
}
.uniui-more-filled:before {
content: "\e64e";
}
.uniui-undo:before {
content: "\e64f";
}
.uniui-images:before {
content: "\e650";
}
.uniui-paperclip:before {
content: "\e652";
}
.uniui-settings:before {
content: "\e653";
}
.uniui-search:before {
content: "\e654";
}
.uniui-redo-filled:before {
content: "\e655";
}
.uniui-list:before {
content: "\e644";
}
.uniui-mail-open-filled:before {
content: "\e63a";
}
.uniui-hand-down-filled:before {
content: "\e63c";
}
.uniui-hand-down:before {
content: "\e63d";
}
.uniui-hand-up-filled:before {
content: "\e63e";
}
.uniui-hand-up:before {
content: "\e63f";
}
.uniui-heart-filled:before {
content: "\e641";
}
.uniui-mail-open:before {
content: "\e643";
}
.uniui-heart:before {
content: "\e639";
}
.uniui-loop:before {
content: "\e633";
}
.uniui-pulldown:before {
content: "\e632";
}
.uniui-scan:before {
content: "\e62a";
}
.uniui-bars:before {
content: "\e627";
}
.uniui-cart-filled:before {
content: "\e629";
}
.uniui-checkbox:before {
content: "\e62b";
}
.uniui-checkbox-filled:before {
content: "\e62c";
}
.uniui-shop:before {
content: "\e62f";
}
.uniui-headphones:before {
content: "\e630";
}
.uniui-cart:before {
content: "\e631";
}

Binary file not shown.

View File

@@ -0,0 +1,5 @@
{
"uni-load-more.contentdown": "Pull up to show more",
"uni-load-more.contentrefresh": "loading...",
"uni-load-more.contentnomore": "No more data"
}

View File

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

View File

@@ -0,0 +1,5 @@
{
"uni-load-more.contentdown": "上拉显示更多",
"uni-load-more.contentrefresh": "正在加载...",
"uni-load-more.contentnomore": "没有更多数据了"
}

View File

@@ -0,0 +1,5 @@
{
"uni-load-more.contentdown": "上拉顯示更多",
"uni-load-more.contentrefresh": "正在加載...",
"uni-load-more.contentnomore": "沒有更多數據了"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,407 @@
<template>
<view class="uni-pagination">
<!-- #ifndef APP-NVUE -->
<view class="uni-pagination__total is-phone-hide"> {{ total }} </view>
<!-- #endif -->
<view
class="uni-pagination__btn"
:class="currentIndex === 1 ? 'uni-pagination--disabled' : 'uni-pagination--enabled'"
:hover-class="currentIndex === 1 ? '' : 'uni-pagination--hover'"
:hover-start-time="20"
:hover-stay-time="70"
@click="clickLeft"
>
<template v-if="showIcon === true || showIcon === 'true'">
<text class="iconfont iconqianhou1"></text>
</template>
<template v-else>
<text class="uni-pagination__child-btn">{{ prevPageText }}</text>
</template>
</view>
<view class="uni-pagination__num uni-pagination__num-flex-none">
<view class="uni-pagination__num-current">
<text class="uni-pagination__num-current-text is-pc-hide text-color">{{ currentIndex }}</text>
<text class="uni-pagination__num-current-text is-pc-hide">/{{ maxPage || 0 }}</text>
<!-- #ifndef APP-NVUE -->
<view
v-for="(item, index) in paper" :key="index"
:class="{ 'page--active': item === currentIndex }"
class="uni-pagination__num-tag tag--active is-phone-hide"
@click.top="selectPage(item, index)"
>
<text>{{ item }}</text>
</view>
<!-- #endif -->
</view>
</view>
<view
class="uni-pagination__btn"
:class="currentIndex >= maxPage ? 'uni-pagination--disabled' : 'uni-pagination--enabled'"
:hover-class="currentIndex === maxPage ? '' : 'uni-pagination--hover'"
:hover-start-time="20"
:hover-stay-time="70"
@click="clickRight"
>
<template v-if="showIcon === true || showIcon === 'true'">
<text class="iconfont iconqianhou2"></text>
</template>
<template v-else>
<text class="uni-pagination__child-btn">{{ nextPageText }}</text>
</template>
</view>
</view>
</template>
<script>
/**
* Pagination 分页器
* @description 分页器组件,用于展示页码、请求数据等
* @tutorial https://ext.dcloud.net.cn/plugin?id=32
* @property {String} prevText 左侧按钮文字
* @property {String} nextText 右侧按钮文字
* @property {Number} current 当前页
* @property {Number} total 数据总量
* @property {Number} pageSize 每页数据量
* @property {Number} showIcon = [true|false] 是否以 icon 形式展示按钮
* @event {Function} change 点击页码按钮时触发 ,e={type,current} current为当前页type值为next/prev表示点击的是上一页还是下一个
*/
export default {
name: 'UniPagination',
emits: ['update:modelValue', 'input', 'change'],
props: {
value: {
type: [Number, String],
default: 1
},
modelValue: {
type: [Number, String],
default: 1
},
prevText: {
type: String
},
nextText: {
type: String
},
current: {
type: [Number, String],
default: 1
},
total: {
// 数据总量
type: [Number, String],
default: 0
},
pageSize: {
// 每页数据量
type: [Number, String],
default: 10
},
showIcon: {
// 是否以 icon 形式展示按钮
type: [Boolean, String],
default: false
},
pagerCount: {
type: Number,
default: 7
}
},
data() {
return {
currentIndex: 1,
paperData: []
};
},
computed: {
prevPageText() {
return this.prevText || '上一页';
},
nextPageText() {
return this.nextText || '下一页';
},
maxPage() {
let maxPage = 1;
let total = Number(this.total);
let pageSize = Number(this.pageSize);
if (total && pageSize) {
maxPage = Math.ceil(total / pageSize);
}
return maxPage;
},
paper() {
const num = this.currentIndex;
// TODO 最大页数
const pagerCount = this.pagerCount;
// const total = 181
const total = this.total;
const pageSize = this.pageSize;
let totalArr = [];
let showPagerArr = [];
let pagerNum = Math.ceil(total / pageSize);
for (let i = 0; i < pagerNum; i++) {
totalArr.push(i + 1);
}
showPagerArr.push(1);
const totalNum = totalArr[totalArr.length - (pagerCount + 1) / 2];
totalArr.forEach((item, index) => {
if ((pagerCount + 1) / 2 >= num) {
if (item < pagerCount + 1 && item > 1) {
showPagerArr.push(item);
}
} else if (num + 2 <= totalNum) {
if (item > num - (pagerCount + 1) / 2 && item < num + (pagerCount + 1) / 2) {
showPagerArr.push(item);
}
} else {
if ((item > num - (pagerCount + 1) / 2 || pagerNum - pagerCount < item) && item < totalArr[totalArr.length - 1]) {
showPagerArr.push(item);
}
}
});
if (pagerNum > pagerCount) {
if ((pagerCount + 1) / 2 >= num) {
showPagerArr[showPagerArr.length - 1] = '...';
} else if (num + 2 <= totalNum) {
showPagerArr[1] = '...';
showPagerArr[showPagerArr.length - 1] = '...';
} else {
showPagerArr[1] = '...';
}
showPagerArr.push(totalArr[totalArr.length - 1]);
} else {
if ((pagerCount + 1) / 2 >= num) {
} else if (num + 2 <= totalNum) {
} else {
showPagerArr.shift();
showPagerArr.push(totalArr[totalArr.length - 1]);
}
}
return showPagerArr;
}
},
watch: {
current: {
immediate: true,
handler(val, old) {
if (val < 1) {
this.currentIndex = 1;
} else {
this.currentIndex = val;
}
}
},
value: {
immediate: true,
handler(val) {
if (Number(this.current) !== 1) return;
if (val < 1) {
this.currentIndex = 1;
} else {
this.currentIndex = val;
}
}
}
},
methods: {
// 选择标签
selectPage(e, index) {
if (parseInt(e)) {
this.currentIndex = e;
this.change('current');
} else {
let pagerNum = Math.ceil(this.total / this.pageSize);
// let pagerNum = Math.ceil(181 / this.pageSize)
// 上一页
if (index <= 1) {
if (this.currentIndex - 5 > 1) {
this.currentIndex -= 5;
} else {
this.currentIndex = 1;
}
return;
}
// 下一页
if (index >= 6) {
if (this.currentIndex + 5 > pagerNum) {
this.currentIndex = pagerNum;
} else {
this.currentIndex += 5;
}
}
}
},
clickLeft() {
if (Number(this.currentIndex) === 1) {
return;
}
this.currentIndex -= 1;
this.change('prev');
},
clickRight() {
if (Number(this.currentIndex) >= this.maxPage) {
return;
}
this.currentIndex += 1;
this.change('next');
},
change(e) {
this.$emit('input', this.currentIndex);
this.$emit('update:modelValue', this.currentIndex);
this.$emit('change', {
type: e,
current: this.currentIndex
});
}
}
};
</script>
<style lang="scss" scoped>
.uni-pagination {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
position: relative;
overflow: hidden;
flex-direction: row;
justify-content: center;
align-items: center;
}
.uni-pagination__total {
font-size: 0.14rem;
color: #999;
margin-right: 0.15rem;
}
.uni-pagination__btn {
/* #ifndef APP-NVUE */
display: flex;
cursor: pointer;
/* #endif */
padding: 0 0.08rem;
line-height: 0.3rem;
font-size: $uni-font-size-base;
position: relative;
background-color: #f0f0f0;
flex-direction: row;
justify-content: center;
align-items: center;
text-align: center;
border-radius: 0.05rem;
// border-width: .01rem;
// border-style: solid;
// border-color: $uni-border-color;
}
.uni-pagination__child-btn {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
position: relative;
flex-direction: row;
justify-content: center;
align-items: center;
text-align: center;
color: #0f1214;
font-size: 0.12rem;
}
.uni-pagination__num {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
height: 0.3rem;
line-height: 0.3rem;
font-size: $uni-font-size-base;
color: $uni-text-color;
margin: 0 0.05rem;
}
.uni-pagination__num-tag {
cursor: pointer;
min-width: 0.3rem;
margin: 0 0.05rem;
height: 0.3rem;
text-align: center;
line-height: 0.3rem;
// border: .01rem red solid;
color: #666;
border-radius: 0.04rem;
// border-width: .01rem;
// border-style: solid;
// border-color: $uni-border-color;
}
.uni-pagination__num-current {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-pagination__num-current-text {
font-size: 0.15rem;
}
.uni-pagination--enabled {
color: #333333;
opacity: 1;
}
.uni-pagination--disabled {
opacity: 0.5;
/* #ifdef H5 */
cursor: default;
/* #endif */
}
.uni-pagination--hover {
color: rgba(0, 0, 0, 0.6);
background-color: $uni-bg-color-hover;
}
.tag--active:hover {
color: $primary-color;
}
.page--active {
color: #fff;
background-color: $primary-color;
}
.page--active:hover {
color: #fff;
}
/* #ifndef APP-NVUE */
.is-pc-hide {
display: block;
}
.is-phone-hide {
display: none;
}
@media screen and (min-width: 450px) {
.is-pc-hide {
display: none;
}
.is-phone-hide {
display: block;
}
.uni-pagination__num-flex-none {
flex: none;
}
}
/* #endif */
</style>

View File

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

View File

@@ -0,0 +1,26 @@
export default {
data() {
return {
}
},
created(){
this.popup = this.getParent()
},
methods:{
/**
* 获取父元素实例
*/
getParent(name = 'uniPopup') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
}
}

View File

@@ -0,0 +1,436 @@
<template>
<view v-if="showPopup" class="uni-popup" :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']" @touchmove.stop.prevent="clear">
<view @touchstart="touchstart">
<uni-transition key="1" v-if="maskShow" name="mask" mode-class="fade" :styles="maskClass" :duration="duration" :show="showTrans" @click="onTap" />
<uni-transition key="2" :mode-class="ani" name="content" :styles="transClass" :duration="duration"
:show="showTrans" @click="onTap">
<view class="uni-popup__wrapper" :style="{ backgroundColor: bg }" :class="[popupstyle]" @click="clear">
<slot />
</view>
</uni-transition>
</view>
<!-- #ifdef H5 -->
<keypress v-if="maskShow" @esc="onTap" />
<!-- #endif -->
</view>
</template>
<script>
// #ifdef H5
import keypress from './keypress.js'
// #endif
/**
* PopUp 弹出层
* @description 弹出层组件,为了解决遮罩弹层的问题
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value left 左侧弹出
* @value right 右侧弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} animation = [ture|false] 是否开启动画
* @property {Boolean} maskClick = [ture|false] 蒙版点击是否关闭弹窗
* @property {String} backgroundColor 主窗口背景色
* @property {Boolean} safeArea 是否适配底部安全区
* @event {Function} change 打开关闭弹窗触发e={show: false}
* @event {Function} maskClick 点击遮罩触发
*/
export default {
name: 'uniPopup',
components: {
// #ifdef H5
keypress
// #endif
},
emits: ['change', 'maskClick'],
props: {
// 开启动画
animation: {
type: Boolean,
default: true
},
// 弹出层类型可选值top: 顶部弹出层bottom底部弹出层center全屏弹出层
// message: 消息提示 ; dialog : 对话框
type: {
type: String,
default: 'center'
},
// maskClick
maskClick: {
type: Boolean,
default: true
},
backgroundColor: {
type: String,
default: 'none'
},
safeArea: {
type: Boolean,
default: true
},
customPosition: {
type: Object,
default () {
return {}
}
}
},
watch: {
/**
* 监听type类型
*/
type: {
handler: function(type) {
if (!this.config[type]) return
this[this.config[type]](true)
},
immediate: true
},
isDesktop: {
handler: function(newVal) {
if (!this.config[newVal]) return
this[this.config[this.type]](true)
},
immediate: true
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick: {
handler: function(val) {
this.mkclick = val
},
immediate: true
}
},
data() {
return {
duration: 300,
ani: [],
showPopup: false,
showTrans: false,
popupWidth: 0,
popupHeight: 0,
config: {
top: 'top',
bottom: 'bottom',
center: 'center',
left: 'left',
right: 'right',
message: 'top',
dialog: 'center',
share: 'bottom',
custom: 'custom'
},
maskClass: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)'
},
transClass: {
position: 'fixed',
left: 0,
right: 0
},
maskShow: true,
mkclick: true,
popupstyle: this.isDesktop ? 'fixforpc-top' : 'top',
callback: null
}
},
computed: {
isDesktop() {
return this.popupWidth >= 500 && this.popupHeight >= 500
},
bg() {
if (this.backgroundColor === '' || this.backgroundColor === 'none') {
return 'transparent'
}
return this.backgroundColor
}
},
mounted() {
const fixSize = () => {
const {
windowWidth,
windowHeight,
windowTop,
safeAreaInsets
} = uni.getSystemInfoSync()
this.popupWidth = windowWidth
this.popupHeight = windowHeight + windowTop
// 是否适配底部安全区
if (this.safeArea) {
this.safeAreaInsets = safeAreaInsets
} else {
this.safeAreaInsets = 0
}
}
fixSize()
// #ifdef H5
// window.addEventListener('resize', fixSize)
// this.$once('hook:beforeDestroy', () => {
// window.removeEventListener('resize', fixSize)
// })
// #endif
},
created() {
this.mkclick = this.maskClick
if (this.animation) {
this.duration = 300
} else {
this.duration = 0
}
// TODO 处理 message 组件生命周期异常的问题
this.messageChild = null
// TODO 解决头条冒泡的问题
this.clearPropagation = false
},
methods: {
/**
* 公用方法,不显示遮罩层
*/
closeMask() {
this.maskShow = false
},
/**
* 公用方法,遮罩层禁止点击
*/
disableMask() {
this.mkclick = false
},
// TODO nvue 取消冒泡
clear(e) {
// #ifndef APP-NVUE
e.stopPropagation()
// #endif
this.clearPropagation = true
},
open(direction, callback) {
let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'custom', 'share']
if (!(direction && innerType.indexOf(direction) !== -1)) {
direction = this.type
}
if (!this.config[direction]) {
console.error('缺少类型:', direction)
return
}
if (callback) this.callback = callback;
this[this.config[direction]]()
this.$emit('change', {
show: true,
type: direction
})
},
close(type) {
this.showTrans = false
this.$emit('change', {
show: false,
type: this.type
})
clearTimeout(this.timer)
// // 自定义关闭事件
// this.customOpen && this.customClose()
this.timer = setTimeout(() => {
this.showPopup = false
}, 300)
if (this.callback) this.callback.call(this);
},
// TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
touchstart() {
this.clearPropagation = false
},
onTap() {
if (this.clearPropagation) {
// fix by mehaotian 兼容 nvue
this.clearPropagation = false
return
}
this.$emit('maskClick')
if (!this.mkclick) return
this.close()
},
/**
* 顶部弹出样式处理
*/
custom(type) {
this.popupstyle = 'center'
this.ani = ['zoom-out', 'fade']
this.transClass = this.customPosition;
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
/**
* 顶部弹出样式处理
*/
top(type) {
this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
this.ani = ['slide-top']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
backgroundColor: this.bg
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
this.$nextTick(() => {
if (this.messageChild && this.type === 'message') {
this.messageChild.timerClose()
}
})
},
/**
* 底部弹出样式处理
*/
bottom(type) {
this.popupstyle = 'bottom'
this.ani = ['slide-bottom']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
paddingBottom: (this.safeAreaInsets && this.safeAreaInsets.bottom) || 0,
backgroundColor: this.bg
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
/**
* 中间弹出样式处理
*/
center(type) {
this.popupstyle = 'center'
this.ani = ['zoom-out', 'fade']
this.transClass = {
position: 'fixed',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
bottom: 0,
left: 0,
right: 0,
top: 0,
justifyContent: 'center',
alignItems: 'center'
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
left(type) {
this.popupstyle = 'left'
this.ani = ['slide-left']
this.transClass = {
position: 'fixed',
left: 0,
bottom: 0,
top: 0,
backgroundColor: this.bg,
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
right(type) {
this.popupstyle = 'right'
this.ani = ['slide-right']
this.transClass = {
position: 'fixed',
bottom: 0,
right: 0,
top: 0,
backgroundColor: this.bg,
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
}
}
}
</script>
<style lang="scss" scoped>
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
&.top,
&.left,
&.right {
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.uni-popup__wrapper {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
/* #endif */
&.left,
&.right {
/* #ifdef H5 */
padding-top: var(--window-top);
/* #endif */
/* #ifndef H5 */
padding-top: 0;
/* #endif */
flex: 1;
}
}
}
.fixforpc-z-index {
/* #ifndef APP-NVUE */
z-index: 999;
/* #endif */
}
.fixforpc-top {
top: 0;
}
</style>

View File

@@ -0,0 +1,454 @@
<template>
<view class="uni-table-scroll" :class="{ 'table--border': border, 'border-none': !noData }">
<!-- #ifdef H5 -->
<table class="uni-table" border="0" cellpadding="0" cellspacing="0" :class="{ 'table--stripe': stripe }" :style="{ 'min-width': minWidth + 'px' }">
<slot></slot>
<view v-if="noData" class="uni-table-loading">
<view class="uni-table-text" :class="{ 'empty-border': border }">{{ emptyText }}</view>
</view>
<view v-if="loading" class="uni-table-mask" :class="{ 'empty-border': border }"><div class="uni-table--loader"></div></view>
</table>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="uni-table" :style="{ 'min-width': minWidth + 'px' }" :class="{ 'table--stripe': stripe }">
<slot></slot>
<view v-if="noData" class="uni-table-loading">
<view class="uni-table-text" :class="{ 'empty-border': border }">{{ emptyText }}</view>
</view>
<view v-if="loading" class="uni-table-mask" :class="{ 'empty-border': border }"><div class="uni-table--loader"></div></view>
</view>
<!-- #endif -->
</view>
</template>
<script>
/**
* Table 表格
* @description 用于展示多条结构类似的数据
* @tutorial https://ext.dcloud.net.cn/plugin?id=3270
* @property {Boolean} border 是否带有纵向边框
* @property {Boolean} stripe 是否显示斑马线
* @property {Boolean} type 是否开启多选
* @property {String} emptyText 空数据时显示的文本内容
* @property {Boolean} loading 显示加载中
* @event {Function} selection-change 开启多选时,当选择项发生变化时会触发该事件
*/
export default {
name: 'uniTable',
options: {
virtualHost: true
},
emits:['selection-change'],
props: {
data: {
type: Array,
default() {
return []
}
},
// 是否有竖线
border: {
type: Boolean,
default: false
},
// 是否显示斑马线
stripe: {
type: Boolean,
default: false
},
// 多选
type: {
type: String,
default: ''
},
// 没有更多数据
emptyText: {
type: String,
default: '没有更多数据'
},
loading: {
type: Boolean,
default: false
},
rowKey: {
type: String,
default: ''
}
},
data() {
return {
noData: true,
minWidth: 0,
multiTableHeads: []
}
},
watch: {
loading(val) {},
data(newVal) {
let theadChildren = this.theadChildren
let rowspan = 1
if (this.theadChildren) {
rowspan = this.theadChildren.rowspan
}
// this.trChildren.length - rowspan
this.noData = false
// this.noData = newVal.length === 0
}
},
created() {
// 定义tr的实例数组
this.trChildren = []
this.thChildren = []
this.theadChildren = null
this.backData = []
this.backIndexData = []
},
methods: {
isNodata() {
let theadChildren = this.theadChildren
let rowspan = 1
if (this.theadChildren) {
rowspan = this.theadChildren.rowspan
}
this.noData = this.trChildren.length - rowspan <= 0
},
/**
* 选中所有
*/
selectionAll() {
let startIndex = 1
let theadChildren = this.theadChildren
if (!this.theadChildren) {
theadChildren = this.trChildren[0]
} else {
startIndex = theadChildren.rowspan - 1
}
let isHaveData = this.data && this.data.length.length > 0
theadChildren.checked = true
theadChildren.indeterminate = false
this.trChildren.forEach((item, index) => {
if (!item.disabled) {
item.checked = true
if (isHaveData && item.keyValue) {
const row = this.data.find(v => v[this.rowKey] === item.keyValue)
if (!this.backData.find(v => v[this.rowKey] === row[this.rowKey])) {
this.backData.push(row)
}
}
if (index > (startIndex - 1) && this.backIndexData.indexOf(index - startIndex) === -1) {
this.backIndexData.push(index - startIndex)
}
}
})
// this.backData = JSON.parse(JSON.stringify(this.data))
this.$emit('selection-change', {
detail: {
value: this.backData,
index: this.backIndexData
}
})
},
/**
* 用于多选表格切换某一行的选中状态如果使用了第二个参数则是设置这一行选中与否selected 为 true 则选中)
*/
toggleRowSelection(row, selected) {
// if (!this.theadChildren) return
row = [].concat(row)
this.trChildren.forEach((item, index) => {
// if (item.keyValue) {
const select = row.findIndex(v => {
//
if (typeof v === 'number') {
return v === index - 1
} else {
return v[this.rowKey] === item.keyValue
}
})
let ischeck = item.checked
if (select !== -1) {
if (typeof selected === 'boolean') {
item.checked = selected
} else {
item.checked = !item.checked
}
if (ischeck !== item.checked) {
this.check(item.rowData||item, item.checked, item.rowData?item.keyValue:null, true)
}
}
// }
})
this.$emit('selection-change', {
detail: {
value: this.backData,
index:this.backIndexData
}
})
},
/**
* 用于多选表格,清空用户的选择
*/
clearSelection() {
let theadChildren = this.theadChildren
if (!this.theadChildren) {
theadChildren = this.trChildren[0]
}
// if (!this.theadChildren) return
theadChildren.checked = false
theadChildren.indeterminate = false
this.trChildren.forEach(item => {
// if (item.keyValue) {
item.checked = false
// }
})
this.backData = []
this.backIndexData = []
this.$emit('selection-change', {
detail: {
value: [],
index: []
}
})
},
/**
* 用于多选表格,切换所有行的选中状态
*/
toggleAllSelection() {
let list = []
let startIndex = 1
let theadChildren = this.theadChildren
if (!this.theadChildren) {
theadChildren = this.trChildren[0]
} else {
startIndex = theadChildren.rowspan - 1
}
this.trChildren.forEach((item, index) => {
if (!item.disabled) {
if (index > (startIndex - 1) ) {
list.push(index-startIndex)
}
}
})
this.toggleRowSelection(list)
},
/**
* 选中\取消选中
* @param {Object} child
* @param {Object} check
* @param {Object} rowValue
*/
check(child, check, keyValue, emit) {
let theadChildren = this.theadChildren
if (!this.theadChildren) {
theadChildren = this.trChildren[0]
}
let childDomIndex = this.trChildren.findIndex((item, index) => child === item)
if(childDomIndex < 0){
childDomIndex = this.data.findIndex(v=>v[this.rowKey] === keyValue) + 1
}
const dataLen = this.trChildren.filter(v => !v.disabled && v.keyValue).length
if (childDomIndex === 0) {
check ? this.selectionAll() : this.clearSelection()
return
}
if (check) {
if (keyValue) {
this.backData.push(child)
}
this.backIndexData.push(childDomIndex - 1)
} else {
const index = this.backData.findIndex(v => v[this.rowKey] === keyValue)
const idx = this.backIndexData.findIndex(item => item === childDomIndex - 1)
if (keyValue) {
this.backData.splice(index, 1)
}
this.backIndexData.splice(idx, 1)
}
const domCheckAll = this.trChildren.find((item, index) => index > 0 && !item.checked && !item.disabled)
if (!domCheckAll) {
theadChildren.indeterminate = false
theadChildren.checked = true
} else {
theadChildren.indeterminate = true
theadChildren.checked = false
}
if (this.backIndexData.length === 0) {
theadChildren.indeterminate = false
}
if (!emit) {
this.$emit('selection-change', {
detail: {
value: this.backData,
index: this.backIndexData
}
})
}
}
}
}
</script>
<style lang="scss">
$border-color: #ebeef5;
.uni-table-scroll {
width: 100%;
/* #ifndef APP-NVUE */
overflow-x: auto;
/* #endif */
}
.uni-table {
position: relative;
width: 100%;
border-radius: 5px;
// box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.1);
background-color: #fff;
/* #ifndef APP-NVUE */
box-sizing: border-box;
display: table;
overflow-x: auto;
::v-deep .uni-table-tr:nth-child(n + 2) {
&:hover {
background-color: #f5f7fa;
}
}
::v-deep .uni-table-thead {
.uni-table-tr {
// background-color: #f5f7fa;
&:hover {
background-color:#fafafa;
}
}
}
/* #endif */
}
.table--border {
border: 1px $border-color solid;
border-right: none;
}
.border-none {
/* #ifndef APP-NVUE */
border-bottom: none;
/* #endif */
}
.table--stripe {
/* #ifndef APP-NVUE */
::v-deep .uni-table-tr:nth-child(2n + 3) {
background-color: #fafafa;
}
/* #endif */
}
/* 表格加载、无数据样式 */
.uni-table-loading {
position: relative;
/* #ifndef APP-NVUE */
display: table-row;
/* #endif */
height: 50px;
line-height: 50px;
overflow: hidden;
box-sizing: border-box;
}
.empty-border {
border-right: 1px $border-color solid;
}
.uni-table-text {
position: absolute;
right: 0;
left: 0;
text-align: center;
font-size: 14px;
color: #999;
}
.uni-table-mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 99;
/* #ifndef APP-NVUE */
display: flex;
margin: auto;
transition: all 0.5s;
/* #endif */
justify-content: center;
align-items: center;
}
.uni-table--loader {
width: 30px;
height: 30px;
border: 2px solid #aaa;
// border-bottom-color: transparent;
border-radius: 50%;
/* #ifndef APP-NVUE */
animation: 2s uni-table--loader linear infinite;
/* #endif */
position: relative;
}
@keyframes uni-table--loader {
0% {
transform: rotate(360deg);
}
10% {
border-left-color: transparent;
}
20% {
border-bottom-color: transparent;
}
30% {
border-right-color: transparent;
}
40% {
border-top-color: transparent;
}
50% {
transform: rotate(0deg);
}
60% {
border-top-color: transparent;
}
70% {
border-left-color: transparent;
}
80% {
border-bottom-color: transparent;
}
90% {
border-right-color: transparent;
}
100% {
transform: rotate(-360deg);
}
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<!-- #ifdef H5 -->
<tbody>
<slot></slot>
</tbody>
<!-- #endif -->
<!-- #ifndef H5 -->
<view><slot></slot></view>
<!-- #endif -->
</template>
<script>
export default {
name: 'uniBody',
options: {
virtualHost: true
},
data() {
return {
}
},
created() {},
methods: {}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,90 @@
<template>
<!-- #ifdef H5 -->
<td class="uni-table-td" :rowspan="rowspan" :colspan="colspan" :class="{'table--border':border}" :style="{width:width + 'px','text-align':align}">
<slot></slot>
</td>
<!-- #endif -->
<!-- #ifndef H5 -->
<!-- :class="{'table--border':border}" -->
<view class="uni-table-td" :class="{'table--border':border}" :style="{width:width + 'px','text-align':align}">
<slot></slot>
</view>
<!-- #endif -->
</template>
<script>
/**
* Td 单元格
* @description 表格中的标准单元格组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=3270
* @property {Number} align = [left|center|right] 单元格对齐方式
*/
export default {
name: 'uniTd',
options: {
virtualHost: true
},
props: {
width: {
type: [String, Number],
default: ''
},
align: {
type: String,
default: 'left'
},
rowspan: {
type: [Number,String],
default: 1
},
colspan: {
type: [Number,String],
default: 1
}
},
data() {
return {
border: false
};
},
created() {
this.root = this.getTable()
this.border = this.root.border
},
methods: {
/**
* 获取父元素实例
*/
getTable() {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== 'uniTable') {
parent = parent.$parent;
if (!parent) return false;
parentName = parent.$options.name;
}
return parent;
},
}
}
</script>
<style lang="scss">
$border-color:#EBEEF5;
.uni-table-td {
display: table-cell;
padding: 8px 10px;
font-size: 14px;
border-bottom: 1px $border-color solid;
font-weight: 400;
color: #606266;
line-height: 23px;
box-sizing: border-box;
}
.table--border {
border-right: 1px $border-color solid;
}
</style>

View File

@@ -0,0 +1,499 @@
<template>
<view class="uni-filter-dropdown">
<view class="dropdown-btn" @click="onDropdown">
<view class="icon-select" :class="{active: canReset}" v-if="isSelect || isRange"></view>
<view class="icon-search" :class="{active: canReset}" v-if="isSearch">
<view class="icon-search-0"></view>
<view class="icon-search-1"></view>
</view>
<view class="icon-calendar" :class="{active: canReset}" v-if="isDate">
<view class="icon-calendar-0"></view>
<view class="icon-calendar-1"></view>
</view>
</view>
<view class="uni-dropdown-cover" v-if="isOpened" @click="handleClose"></view>
<view class="dropdown-popup dropdown-popup-right" v-if="isOpened" @click.stop>
<!-- select-->
<view v-if="isSelect" class="list">
<label class="flex-r a-i-c list-item" v-for="(item,index) in dataList" :key="index" @click="onItemClick($event, index)">
<check-box class="check" :checked="item.checked" />
<view class="checklist-content">
<text class="checklist-text" :style="item.styleIconText">{{item[map.text]}}</text>
</view>
</label>
</view>
<view v-if="isSelect" class="flex-r opera-area">
<view class="flex-f btn btn-default" :class="{disable: !canReset}" @click="handleSelectReset">{{resource.reset}}</view>
<view class="flex-f btn btn-submit" @click="handleSelectSubmit">{{resource.submit}}</view>
</view>
<!-- search -->
<view v-if="isSearch" class="search-area">
<input class="search-input" v-model="filterValue" />
</view>
<view v-if="isSearch" class="flex-r opera-area">
<view class="flex-f btn btn-submit" @click="handleSearchSubmit">{{resource.search}}</view>
<view class="flex-f btn btn-default" :class="{disable: !canReset}" @click="handleSearchReset">{{resource.reset}}</view>
</view>
<!-- range -->
<view v-if="isRange">
<view class="input-label">{{resource.gt}}</view>
<input class="input" v-model="gtValue" />
<view class="input-label">{{resource.lt}}</view>
<input class="input" v-model="ltValue" />
</view>
<view v-if="isRange" class="flex-r opera-area">
<view class="flex-f btn btn-default" :class="{disable: !canReset}" @click="handleRangeReset">{{resource.reset}}</view>
<view class="flex-f btn btn-submit" @click="handleRangeSubmit">{{resource.submit}}</view>
</view>
<!-- date -->
<view v-if="isDate">
<uni-datetime-picker ref="datetimepicker" :value="dateRange" type="datetimerange" return-type="timestamp" @change="datetimechange" @maskClick="timepickerclose">
<view></view>
</uni-datetime-picker>
</view>
</view>
</view>
</template>
<script>
import checkBox from '../uni-tr/table-checkbox.vue'
const resource = {
"reset": "重置",
"search": "搜索",
"submit": "确定",
"filter": "筛选",
"gt": "大于等于",
"lt": "小于等于",
"date": "日期范围"
}
const DropdownType = {
Select: "select",
Search: "search",
Range: "range",
Date: "date",
Timestamp: "timestamp"
}
export default {
name: 'FilterDropdown',
emits:['change'],
components: {
checkBox
},
options: {
virtualHost: true
},
props: {
filterType: {
type: String,
default: DropdownType.Select
},
filterData: {
type: Array,
default () {
return []
}
},
mode: {
type: String,
default: 'default'
},
map: {
type: Object,
default () {
return {
text: 'text',
value: 'value'
}
}
}
},
computed: {
canReset() {
if (this.isSearch) {
return this.filterValue.length > 0
}
if (this.isSelect) {
return this.checkedValues.length > 0
}
if (this.isRange) {
return (this.gtValue.length > 0 && this.ltValue.length > 0)
}
if (this.isDate) {
return this.dateSelect.length > 0
}
return false
},
isSelect() {
return this.filterType === DropdownType.Select
},
isSearch() {
return this.filterType === DropdownType.Search
},
isRange() {
return this.filterType === DropdownType.Range
},
isDate() {
return (this.filterType === DropdownType.Date || this.filterType === DropdownType.Timestamp)
}
},
watch: {
filterData(newVal) {
this._copyFilters()
},
indeterminate(newVal) {
this.isIndeterminate = newVal
}
},
data() {
return {
resource,
enabled: true,
isOpened: false,
dataList: [],
filterValue: '',
checkedValues: [],
gtValue: '',
ltValue: '',
dateRange: [],
dateSelect: []
};
},
created() {
this._copyFilters()
},
methods: {
_copyFilters() {
let dl = JSON.parse(JSON.stringify(this.filterData))
for (let i = 0; i < dl.length; i++) {
if (dl[i].checked === undefined) {
dl[i].checked = false
}
}
this.dataList = dl
},
openPopup() {
this.isOpened = true
if (this.isDate) {
this.$nextTick(() => {
if (!this.dateRange.length) {
this.resetDate()
}
this.$refs.datetimepicker.show()
})
}
},
closePopup() {
this.isOpened = false
},
handleClose(e) {
this.closePopup()
},
resetDate() {
let date = new Date()
let dateText = date.toISOString().split('T')[0]
this.dateRange = [dateText + ' 0:00:00', dateText + ' 23:59:59']
},
onDropdown(e) {
this.openPopup()
},
onItemClick(e, index) {
let items = this.dataList
let listItem = items[index]
if (listItem.checked === undefined) {
items[index].checked = true
} else {
items[index].checked = !listItem.checked
}
let checkvalues = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.checked) {
checkvalues.push(item.value)
}
}
this.checkedValues = checkvalues
},
datetimechange(e) {
this.closePopup()
this.dateRange = e
this.dateSelect = e
this.$emit('change', {
filterType: this.filterType,
filter: e
})
},
timepickerclose(e) {
this.closePopup()
},
handleSelectSubmit() {
this.closePopup()
this.$emit('change', {
filterType: this.filterType,
filter: this.checkedValues
})
},
handleSelectReset() {
if (!this.canReset) {
return;
}
var items = this.dataList
for (let i = 0; i < items.length; i++) {
let item = items[i]
this.$set(item, 'checked', false)
}
this.checkedValues = []
this.handleSelectSubmit()
},
handleSearchSubmit() {
this.closePopup()
this.$emit('change', {
filterType: this.filterType,
filter: this.filterValue
})
},
handleSearchReset() {
if (!this.canReset) {
return;
}
this.filterValue = ''
this.handleSearchSubmit()
},
handleRangeSubmit(isReset) {
this.closePopup()
this.$emit('change', {
filterType: this.filterType,
filter: isReset === true ? [] : [parseInt(this.gtValue), parseInt(this.ltValue)]
})
},
handleRangeReset() {
if (!this.canReset) {
return;
}
this.gtValue = ''
this.ltValue = ''
this.handleRangeSubmit(true)
}
}
}
</script>
<style lang="scss">
.flex-r {
display: flex;
flex-direction: row;
}
.flex-f {
flex: 1;
}
.a-i-c {
align-items: center;
}
.j-c-c {
justify-content: center;
}
.icon-select {
width: 14px;
height: 16px;
border: solid 6px transparent;
border-top: solid 6px #ddd;
border-bottom: none;
background-color: #ddd;
background-clip: content-box;
box-sizing: border-box;
}
.icon-select.active {
background-color: $primary-color;
border-top-color: $primary-color;
}
.icon-search {
width: 12px;
height: 16px;
position: relative;
}
.icon-search-0 {
border: 2px solid #ddd;
border-radius: 8px;
width: 7px;
height: 7px;
}
.icon-search-1 {
position: absolute;
top: 8px;
right: 0;
width: 1px;
height: 7px;
background-color: #ddd;
transform: rotate(-45deg);
}
.icon-search.active .icon-search-0 {
border-color: $primary-color;
}
.icon-search.active .icon-search-1 {
background-color: $primary-color;
}
.icon-calendar {
color: #ddd;
width: 14px;
height: 16px;
}
.icon-calendar-0 {
height: 4px;
margin-top: 3px;
margin-bottom: 1px;
background-color: #ddd;
border-radius: 2px 2px 1px 1px;
position: relative;
}
.icon-calendar-0:before, .icon-calendar-0:after {
content: '';
position: absolute;
top: -3px;
width: 4px;
height: 3px;
border-radius: 1px;
background-color: #ddd;
}
.icon-calendar-0:before {
left: 2px;
}
.icon-calendar-0:after {
right: 2px;
}
.icon-calendar-1 {
height: 9px;
background-color: #ddd;
border-radius: 1px 1px 2px 2px;
}
.icon-calendar.active {
color: $primary-color;
}
.icon-calendar.active .icon-calendar-0,
.icon-calendar.active .icon-calendar-1,
.icon-calendar.active .icon-calendar-0:before,
.icon-calendar.active .icon-calendar-0:after {
background-color: $primary-color;
}
.uni-filter-dropdown {
position: relative;
font-weight: normal;
}
.dropdown-popup {
position: absolute;
top: 100%;
background-color: #fff;
box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
min-width: 150px;
z-index: 1000;
}
.dropdown-popup-left {
left: 0;
}
.dropdown-popup-right {
right: 0;
}
.uni-dropdown-cover {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: transparent;
z-index: 100;
}
.list {
margin-top: 5px;
margin-bottom: 5px;
}
.list-item {
padding: 5px 10px;
text-align: left;
}
.list-item:hover {
background-color: #f0f0f0;
}
.check {
margin-right: 5px;
}
.search-area {
padding: 10px;
}
.search-input {
font-size: 12px;
border: 1px solid #f0f0f0;
border-radius: 3px;
padding: 2px 5px;
min-width: 150px;
text-align: left;
}
.input-label {
margin: 10px 10px 5px 10px;
text-align: left;
}
.input {
font-size: 12px;
border: 1px solid #f0f0f0;
border-radius: 3px;
margin: 10px;
padding: 2px 5px;
min-width: 150px;
text-align: left;
}
.opera-area {
cursor: default;
border-top: 1px solid #ddd;
padding: 5px;
}
.opera-area .btn {
font-size: 12px;
border-radius: 3px;
margin: 5px;
padding: 4px 4px;
}
.btn-default {
border: 1px solid #ddd;
}
.btn-default.disable {
border-color: transparent;
}
.btn-submit {
background-color: $primary-color;
color: #ffffff;
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<!-- #ifdef H5 -->
<th :rowspan="rowspan" :colspan="colspan" class="uni-table-th" :class="{ 'table--border': border }" :style="{ width: customWidth + 'px', 'text-align': align }">
<view class="uni-table-th-row">
<view class="uni-table-th-content" :style="{ 'justify-content': contentAlign }" @click="sort">
<slot></slot>
<view v-if="sortable" class="arrow-box">
<text class="arrow up" :class="{ active: ascending }" @click.stop="ascendingFn"></text>
<text class="arrow down" :class="{ active: descending }" @click.stop="descendingFn"></text>
</view>
</view>
<dropdown v-if="filterType || filterData.length" :filterData="filterData" :filterType="filterType" @change="ondropdown"></dropdown>
</view>
</th>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="uni-table-th" :class="{ 'table--border': border }" :style="{ width: customWidth + 'px', 'text-align': align }"><slot></slot></view>
<!-- #endif -->
</template>
<script>
// #ifdef H5
import dropdown from './filter-dropdown.vue'
// #endif
/**
* Th 表头
* @description 表格内的表头单元格组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=3270
* @property {Number | String} width 单元格宽度支持纯数字、携带单位px或rpx
* @property {Boolean} sortable 是否启用排序
* @property {Number} align = [left|center|right] 单元格对齐方式
* @value left 单元格文字左侧对齐
* @value center 单元格文字居中
* @value right 单元格文字右侧对齐
* @property {Array} filterData 筛选数据
* @property {String} filterType [search|select] 筛选类型
* @value search 关键字搜素
* @value select 条件选择
* @event {Function} sort-change 排序触发事件
*/
export default {
name: 'uniTh',
options: {
virtualHost: true
},
components: {
// #ifdef H5
dropdown
// #endif
},
emits:['sort-change','filter-change'],
props: {
width: {
type: [String, Number],
default: ''
},
align: {
type: String,
default: 'left'
},
rowspan: {
type: [Number, String],
default: 1
},
colspan: {
type: [Number, String],
default: 1
},
sortable: {
type: Boolean,
default: false
},
filterType: {
type: String,
default: ""
},
filterData: {
type: Array,
default () {
return []
}
}
},
data() {
return {
border: false,
ascending: false,
descending: false
}
},
computed: {
// 根据props中的width属性 自动匹配当前th的宽度(px)
customWidth(){
if(typeof this.width === 'number'){
return this.width
} else if(typeof this.width === 'string') {
let regexHaveUnitPx = new RegExp(/^[1-9][0-9]*px$/g)
let regexHaveUnitRpx = new RegExp(/^[1-9][0-9]*rpx$/g)
let regexHaveNotUnit = new RegExp(/^[1-9][0-9]*$/g)
if (this.width.match(regexHaveUnitPx) !== null) { // 携带了 px
return this.width.replace('px', '')
} else if (this.width.match(regexHaveUnitRpx) !== null) { // 携带了 rpx
let numberRpx = Number(this.width.replace('rpx', ''))
let widthCoe = uni.getSystemInfoSync().screenWidth / 750
return Math.round(numberRpx * widthCoe)
} else if (this.width.match(regexHaveNotUnit) !== null) { // 未携带 rpx或px 的纯数字 String
return this.width
} else { // 不符合格式
return ''
}
} else {
return ''
}
},
contentAlign() {
let align = 'left'
switch (this.align) {
case 'left':
align = 'flex-start'
break
case 'center':
align = 'center'
break
case 'right':
align = 'flex-end'
break
}
return align
}
},
created() {
this.root = this.getTable('uniTable')
this.rootTr = this.getTable('uniTr')
this.rootTr.minWidthUpdate(this.customWidth ? this.customWidth : 140)
this.border = this.root.border
this.root.thChildren.push(this)
},
methods: {
sort() {
if (!this.sortable) return
this.clearOther()
if (!this.ascending && !this.descending) {
this.ascending = true
this.$emit('sort-change', { order: 'ascending' })
return
}
if (this.ascending && !this.descending) {
this.ascending = false
this.descending = true
this.$emit('sort-change', { order: 'descending' })
return
}
if (!this.ascending && this.descending) {
this.ascending = false
this.descending = false
this.$emit('sort-change', { order: null })
}
},
ascendingFn() {
this.clearOther()
this.ascending = !this.ascending
this.descending = false
this.$emit('sort-change', { order: this.ascending ? 'ascending' : null })
},
descendingFn() {
this.clearOther()
this.descending = !this.descending
this.ascending = false
this.$emit('sort-change', { order: this.descending ? 'descending' : null })
},
clearOther() {
this.root.thChildren.map(item => {
if (item !== this) {
item.ascending = false
item.descending = false
}
return item
})
},
ondropdown(e) {
this.$emit("filter-change", e)
},
/**
* 获取父元素实例
*/
getTable(name) {
let parent = this.$parent
let parentName = parent.$options.name
while (parentName !== name) {
parent = parent.$parent
if (!parent) return false
parentName = parent.$options.name
}
return parent
}
}
}
</script>
<style lang="scss">
$border-color: #ebeef5;
.uni-table-th {
padding: 12px 10px;
/* #ifndef APP-NVUE */
display: table-cell;
box-sizing: border-box;
/* #endif */
font-size: 14px;
font-weight: bold;
color: #909399;
border-bottom: 1px $border-color solid;
}
.uni-table-th-row {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.table--border {
border-right: 1px $border-color solid;
}
.uni-table-th-content {
display: flex;
align-items: center;
flex: 1;
}
.arrow-box {
}
.arrow {
display: block;
position: relative;
width: 10px;
height: 8px;
// border: 1px red solid;
left: 5px;
overflow: hidden;
cursor: pointer;
}
.down {
top: 3px;
::after {
content: '';
width: 8px;
height: 8px;
position: absolute;
left: 2px;
top: -5px;
transform: rotate(45deg);
background-color: #ccc;
}
&.active {
::after {
background-color: $primary-color;
}
}
}
.up {
::after {
content: '';
width: 8px;
height: 8px;
position: absolute;
left: 2px;
top: 5px;
transform: rotate(45deg);
background-color: #ccc;
}
&.active {
::after {
background-color: $primary-color;
}
}
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<!-- #ifdef H5 -->
<thead class="uni-table-thead">
<tr class="uni-table-tr">
<th :rowspan="rowspan" colspan="1" class="checkbox" :class="{ 'tr-table--border': border }">
<table-checkbox :indeterminate="indeterminate" :checked="checked" @checkboxSelected="checkboxSelected"></table-checkbox>
</th>
</tr>
<slot></slot>
</thead>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="uni-table-thead"><slot></slot></view>
<!-- #endif -->
</template>
<script>
import tableCheckbox from '../uni-tr/table-checkbox.vue'
export default {
name: 'uniThead',
components: {
tableCheckbox
},
options: {
virtualHost: true
},
data() {
return {
border: false,
selection: false,
rowspan: 1,
indeterminate: false,
checked: false
}
},
created() {
this.root = this.getTable()
// #ifdef H5
this.root.theadChildren = this
// #endif
this.border = this.root.border
this.selection = this.root.type
},
methods: {
init(self) {
this.rowspan++
},
checkboxSelected(e) {
this.indeterminate = false
const backIndexData = this.root.backIndexData
const data = this.root.trChildren.filter(v => !v.disabled && v.keyValue)
if (backIndexData.length === data.length) {
this.checked = false
this.root.clearSelection()
} else {
this.checked = true
this.root.selectionAll()
}
},
/**
* 获取父元素实例
*/
getTable(name = 'uniTable') {
let parent = this.$parent
let parentName = parent.$options.name
while (parentName !== name) {
parent = parent.$parent
if (!parent) return false
parentName = parent.$options.name
}
return parent
}
}
}
</script>
<style lang="scss">
$border-color: #ebeef5;
.uni-table-thead {
display: table-header-group;
}
.uni-table-tr {
/* #ifndef APP-NVUE */
display: table-row;
transition: all 0.3s;
box-sizing: border-box;
/* #endif */
border: 1px red solid;
background-color: #fafafa;
}
.checkbox {
padding: 0 8px;
width: 26px;
padding-left: 12px;
/* #ifndef APP-NVUE */
display: table-cell;
vertical-align: middle;
/* #endif */
color: #333;
font-weight: 500;
border-bottom: 1px $border-color solid;
font-size: 14px;
// text-align: center;
}
.tr-table--border {
border-right: 1px $border-color solid;
}
/* #ifndef APP-NVUE */
.uni-table-tr {
::v-deep .uni-table-th {
&.table--border:last-child {
// border-right: none;
}
}
::v-deep .uni-table-td {
&.table--border:last-child {
// border-right: none;
}
}
}
/* #endif */
</style>

View File

@@ -0,0 +1,177 @@
<template>
<view class="uni-table-checkbox" @click="selected">
<view v-if="!indeterminate" class="checkbox__inner" :class="{'is-checked':isChecked,'is-disable':isDisabled}">
<view class="checkbox__inner-icon"></view>
</view>
<view v-else class="checkbox__inner checkbox--indeterminate">
<view class="checkbox__inner-icon"></view>
</view>
</view>
</template>
<script>
export default {
name: 'TableCheckbox',
emits:['checkboxSelected'],
props: {
indeterminate: {
type: Boolean,
default: false
},
checked: {
type: [Boolean,String],
default: false
},
disabled: {
type: Boolean,
default: false
},
index: {
type: Number,
default: -1
},
cellData: {
type: Object,
default () {
return {}
}
}
},
watch:{
checked(newVal){
if(typeof this.checked === 'boolean'){
this.isChecked = newVal
}else{
this.isChecked = true
}
},
indeterminate(newVal){
this.isIndeterminate = newVal
}
},
data() {
return {
isChecked: false,
isDisabled: false,
isIndeterminate:false
}
},
created() {
if(typeof this.checked === 'boolean'){
this.isChecked = this.checked
}
this.isDisabled = this.disabled
},
methods: {
selected() {
if (this.isDisabled) return
this.isIndeterminate = false
this.isChecked = !this.isChecked
this.$emit('checkboxSelected', {
checked: this.isChecked,
data: this.cellData
})
}
}
}
</script>
<style lang="scss">
$checked-color: $primary-color;
$border-color: #DCDFE6;
$disable:0.4;
.uni-table-checkbox {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: relative;
margin: 5px 0;
cursor: pointer;
// 多选样式
.checkbox__inner {
/* #ifndef APP-NVUE */
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
position: relative;
width: 16px;
height: 16px;
border: 1px solid $border-color;
border-radius: 2px;
background-color: #fff;
z-index: 1;
.checkbox__inner-icon {
position: absolute;
/* #ifdef APP-NVUE */
top: 2px;
/* #endif */
/* #ifndef APP-NVUE */
top: 2px;
/* #endif */
left: 5px;
height: 7px;
width: 3px;
border: 1px solid #fff;
border-left: 0;
border-top: 0;
opacity: 0;
transform-origin: center;
transform: rotate(45deg);
box-sizing: content-box;
}
&.checkbox--indeterminate {
border-color: $checked-color;
background-color: $checked-color;
.checkbox__inner-icon {
position: absolute;
opacity: 1;
height: 2px;
top: 0;
margin: auto;
left: 0px;
right: 0px;
bottom: 0;
width: auto;
border: none;
border-radius: 2px;
transform: scale(0.5);
background-color: #fff;
}
}
&:hover{
border-color: $checked-color;
}
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
background-color: #F2F6FC;
border-color: $border-color;
}
// 选中
&.is-checked {
border-color: $checked-color;
background-color: $checked-color;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
// 选中禁用
&.is-disable {
opacity: $disable;
}
}
}
}
</style>

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