初始化产品库

This commit is contained in:
aixianling
2021-11-15 10:29:05 +08:00
parent 8f735a4ffe
commit 5440b43b9c
306 changed files with 54508 additions and 3 deletions

69
src/components/AiAdd.vue Normal file
View File

@@ -0,0 +1,69 @@
<template>
<movable-area class="movableArea">
<movable-view direction="all" x="300" y="500">
<div class="AiAdd" @click.stop="add"></div>
</movable-view>
</movable-area>
</template>
<script>
export default {
name: "AiAdd",
props: {
},
data() {
return {}
},
methods: {
add() {
this.$emit("add")
}
}
}
</script>
<style lang="scss" scoped>
.movableArea {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 999;
uni-movable-view {
pointer-events: auto;
}
}
.AiAdd {
width: 96px;
height: 96px;
background: #1365DD;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
&:before , &:after{
content: "";
background: #FFFFFF;
display: block;
position: absolute;
border-radius: 4px;
}
&:before{
height: 48px;
width: 4px;
}
&:after{
height: 4px;
width: 48px;
}
}
</style>

87
src/components/AiBack.vue Normal file
View File

@@ -0,0 +1,87 @@
<template>
<ai-fixed-btn v-if="!isTopPage||custom">
<div class="AiBack" @click.stop="back">
<img :src="imgHomeUrl + 'back.png'" alt="">
<text>返回</text>
</div>
</ai-fixed-btn>
</template>
<script>
import AiFixedBtn from "./AiFixedBtn";
export default {
name: "AiBack",
components: {AiFixedBtn},
props: {
delta: {
type: Number,
default: 1
},
eventName: {
type: String,
default: ''
},
data: {
type: Object | Boolean,
default: () => {
}
},
custom: Boolean,
visible: Boolean,
},
data() {
return {
isTopPage: false
}
},
methods: {
back() {
if (this.visible)
return this.$parent.$emit(this.eventName, this.data)
if (this.custom) {
this.$emit("back")
} else uni.navigateBack({
delta: this.delta,
success: () => {
if (this.eventName != '') {
uni.$emit(this.eventName, this.data)
}
},
fail: (err) => {
console.error(err)
}
})
}
},
mounted() {
this.isTopPage = window.history.length <= 1
}
}
</script>
<style lang="scss" scoped>
.AiBack {
width: 108px;
height: 108px;
background: #6BA1F9;
box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.12);
border-radius: 50%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
img {
width: 40px;
height: 40px;
}
text {
font-size: 26px;
font-weight: 800;
color: #FFFFFF;
line-height: 40px;
}
}
</style>

115
src/components/AiCard.vue Normal file
View File

@@ -0,0 +1,115 @@
<template>
<section class="AiCard">
<div flex v-if="$slots.custom" class="start">
<div class="fill">
<slot name="custom"/>
</div>
<div v-if="$slots.menu" class="iconfont iconfont-iconMore" @tap.stop="handleMore"/>
</div>
<template v-else>
<u-row>
<div class="content">
<slot/>
</div>
<div btn @tap="$emit('send')">发送</div>
</u-row>
<u-row justify="space-between">
<slot v-if="$slots.title" name="title"/>
<div v-else>{{ cardTitle }}</div>
<div v-if="$slots.menu" class="iconfont iconfont-iconMore" @tap.stop="handleMore"/>
</u-row>
</template>
<div v-if="menu" class="mask" @click="menu=false">
<div class="moreMenu" :style="menuPos">
<slot name="menu"/>
</div>
</div>
</section>
</template>
<script>
export default {
name: "AiCard",
props: {
cardTitle: String
},
data() {
return {
menuPos: {},
menu: false
}
},
methods: {
handleMore({detail}) {
this.menuPos = {
left: detail.x - 10 + 'px',
top: detail.y + 'px'
}
this.menu = !this.menu
}
}
}
</script>
<style lang="scss" scoped>
.AiCard {
width: 100%;
background: #FFFFFF;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
border-radius: 8px;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
color: #999;
font-size: 26px;
flex-shrink: 0;
.content {
background: #F9F9F9;
border-radius: 4px;
min-height: 120px;
flex: 1;
min-width: 0;
margin-bottom: 26px;
padding: 20px;
box-sizing: border-box;
color: #333;
}
.u-row {
flex-wrap: nowrap;
}
div[btn] {
color: #1365DD;
padding: 0 0 0 18px;
cursor: pointer;
}
.iconfont-iconMore {
font-size: 38px;
transform: rotate(90deg);
}
.moreMenu {
position: fixed;
background: #FFFFFF;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
transform: translate(-100%, -100%);
min-width: 100px;
min-height: 100px;
z-index: 9;
}
.mask {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 11;
}
}
</style>

76
src/components/AiCell.vue Normal file
View File

@@ -0,0 +1,76 @@
<template>
<section class="AiCell" :class="{bottomBorder,alignCenter,topLabel}">
<div class="label" :class="{title}">
<slot v-if="$slots.label" name="label"/>
<span v-else>{{ label }}</span>
</div>
<div class="content" :class="{topLabel}">
<slot/>
</div>
</section>
</template>
<script>
export default {
name: "AiCell",
props: {
label: {default: ""},
bottomBorder: Boolean,
topLabel: Boolean,
title: Boolean,
alignCenter: Boolean
}
}
</script>
<style lang="scss" scoped>
.AiCell {
display: flex;
min-height: 72px;
font-size: 30px;
color: #333;
padding: 14px 0;
box-sizing: border-box;
justify-content: space-between;
&.bottomBorder {
border-bottom: 1px solid #eee;
}
&.alignCenter {
align-items: center;
}
&.topLabel {
flex-direction: column;
}
.label {
min-width: 60px;
flex-shrink: 0;
width: auto;
color: #999;
&.title {
color: #333;
font-weight: bold;
font-size: 34px;
}
}
.content {
flex: 1;
min-width: 100px;
min-height: 40px;
max-width: 500px;
text-align: right;
&.topLabel {
text-align: start;
margin-top: 16px;
max-width: 100%;
}
}
}
</style>

60
src/components/AiDate.vue Normal file
View File

@@ -0,0 +1,60 @@
<template>
<section class="AiDate">
<u-calendar v-model="show" @change="handleSelect" :mode="mode"/>
<div flex @click="show=true">
<div v-if="label" v-html="label"/>
<div v-else v-html="placeholder"/>
<i class="iconfont iconfont-iconArrow_Down"/>
</div>
</section>
</template>
<script>
import UCalendar from "../uview/components/u-calendar/u-calendar";
import dayjs from 'dayjs'
export default {
name: "AiDate",
components: {UCalendar},
computed: {
label() {
let arr = (this.selected || this.value)?.toString()?.split(",") || []
arr = arr.map(e => dayjs(e).format("MM-DD").replace("Invalid Date", ''))
return arr.join('至')
}
},
data() {
return {
show: false,
selected: ""
}
},
props: {
value: {default: ""},
placeholder: {default: "请选择"},
mode: {default: "date"},//date 单个日期|range 日期范围
},
methods: {
handleSelect(v) {
if (this.mode == 'date') {
this.selected = v.result
this.$emit('change', v.result)
} else if (this.mode == 'range') {
this.selected = [v.startDate, v.endDate]
this.$emit('change', v)
}
}
}
}
</script>
<style lang="scss" scoped>
.AiDate {
color: #333333;
.iconfont-iconArrow_Down {
margin-left: 4px;
font-size: 32px;
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="emptyWrap">
<img class="emptyImg" src="./static/Empty.png">
<div class="emptyText">{{description}}</div>
</div>
</template>
<script>
export default {
name:"emptyData",
props:{
description:{
default:'暂无相关信息',
type:String
}
}
}
</script>
<style lang="scss" scoped>
.emptyWrap {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.emptyImg{
width: 400rpx;
height: 240rpx;
margin-top: 112px;
}
.emptyText{
font-size:29rpx;
font-family:PingFangSC-Regular,PingFang SC;
font-weight:400;
color:rgba(183,183,183,1);
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,36 @@
<template>
<section class="AiFixedBtn">
<movable-area class="movableArea">
<movable-view direction="all" x="300" y="500" @tap.stop>
<slot/>
</movable-view>
</movable-area>
</section>
</template>
<script>
export default {
name: "AiFixedBtn"
}
</script>
<style lang="scss" scoped>
.AiFixedBtn {
}
.movableArea {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 999;
uni-movable-view {
pointer-events: auto;
width: auto;
height: auto;
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<section class="AiImage">
<div v-if="$slots.default" @tap="prev">
<slot/>
</div>
<u-image v-else :src="src" @tap="prev">
<image v-if="link" class="errorImage" slot="error" :src="$cdn+'link.png'"/>
<image v-else-if="miniapp" class="errorImage" slot="error" :src="$cdn+'miniwxmp.jpg'"/>
<image v-else class="errorImage" slot="error" :src="$cdn+'file.png'"/>
</u-image>
</section>
</template>
<script>
import UImage from "../uview/components/u-image/u-image";
import UModal from "../uview/components/u-modal/u-modal";
import {mapActions} from "vuex";
export default {
name: "AiImage",
components: {UModal, UImage},
data() {
return {
dialog: false
}
},
props: {
src: String,
preview: Boolean,
link: Boolean,
miniapp: Boolean,
file: {
default: () => {
}
}
},
methods: {
...mapActions(['previewFile', 'injectJWeixin']),
prev() {
if (this.preview) {
if (!!this.src) {
uni.previewImage({
current: this.src,
urls: [this.src]
})
} else {
this.previewFile({size: 1, ...this.file})
}
}
},
}
}
</script>
<style lang="scss" scoped>
.AiImage {
::v-deep image {
width: 160px;
height: 160px;
object-fit: cover;
}
::v-deep .u-image__error {
position: relative;
}
.errorImage {
width: 80px;
height: 80px;
}
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<section class="AiLoading">
<image :src="image"/>
<span>{{ tips }}</span>
</section>
</template>
<script>
export default {
name: "AiLoading",
props: {
tips: {default: "应用加载中"},
image: {default: "https://cdn.cunwuyun.cn/wxAdmin/img/message.png"}
}
}
</script>
<style lang="scss" scoped>
.AiLoading {
font-size: 32px;
color: #666;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>

68
src/components/AiMap.vue Normal file
View File

@@ -0,0 +1,68 @@
<template>
<section class="AiMap">
<div ref="amap" class="map"/>
</section>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
export default {
name: "AiMap",
props: {
plugins: {default: () => ['AMap.DistrictSearch']},
map: Object,
lib: Object
},
data() {
return {
amap: null
}
},
methods: {
initMap() {
let {plugins} = this
AMapLoader.load({
key: '54a02a43d9828a8f9cd4f26fe281e74e',
version: '2.0',
plugins
}).then(AMap => {
this.amap = new AMap.Map(this.$refs.amap, {
resizeEnable: true,
zoom: 14,
})
this.$emit('update:lib', AMap)
this.$emit('update:map', this.amap)
})
},
},
mounted() {
this.initMap()
},
destroyed() {
this.amap?.destroy()
}
}
</script>
<style lang="scss" scoped>
.AiMap {
.map {
height: 100%;
}
::v-deep .amap-logo, ::v-deep .amap-copyright {
display: none !important;
}
::v-deep .amap-icon {
width: 40px !important;
height: 40px !important;
img {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<section class="AiResult">
<slot v-if="$slots.default"/>
<template v-else>
<image :src="result.image"/>
<span class="tips">{{ result.tips }}</span>
<slot name="extra" class="extra" v-if="$slots.extra"/>
<div v-if="result.btn" class="btn" @tap="handleTap">{{ result.btn }}</div>
</template>
</section>
</template>
<script>
export default {
name: "AiResult",
props: {
tips: {default: "提交成功!"},
image: {default: "https://cdn.cunwuyun.cn/dvcp/h5/result/success.png"},
btn: {default: ""},
status: {default: "success"},
btnTap: Function
},
computed: {
result() {
let obj = {
image: this.image,
tips: this.tips,
btn: this.btn
}
if (this.status == "error") {
obj.image = this.$cdn + "result/fail.png"
obj.tips = this.tips || "提交失败!"
}
return obj
}
},
methods: {
handleTap() {
this.btnTap && this.btnTap()
}
}
}
</script>
<style lang="scss" scoped>
.AiResult {
padding-top: 96px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: bold;
& > image {
width: 192px;
height: 192px;
}
.tips {
margin: 16px auto 0;
color: #333;
}
.extra {
margin-top: 48px;
}
.btn {
cursor: pointer;
margin-top: 80px;
width: calc(100% - 192px);
height: 88px;
background: #197DF0;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
border-radius: 8px;
color: #FFF;
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,87 @@
<template>
<section class="AiSearchPopup">
<u-popup v-model="show" length="100%" closeable :mode="mode">
<slot v-if="$slots.default"/>
<div class="searchPane" v-else>
<div class="title">{{ title }}</div>
<u-search v-model="search" :placeholder="placeholder" :show-action="false" @search="getList()" :focus="show"/>
<div class="result">
<div class="option" v-for="(op,i) in list" :key="i" @tap="handleSelect(op)">{{ op[ops.label] }}</div>
</div>
</div>
</u-popup>
<div @tap="show=true">
<slot name="btn"/>
</div>
</section>
</template>
<script>
export default {
name: "AiSearchPopup",
props: {
title: {default: "搜索"},
placeholder: {default: "请搜索"},
ops: {default: () => ({label: 'label', search: 'name'})},
url: String,
mode: {default: "right"}
},
data() {
return {
show: false,
search: "",
list: []
}
},
methods: {
getList() {
this.url && this.$instance.post(this.url, null, {
params: {[this.ops.search]: this.search}
}).then(res => {
if (res?.data) {
this.list = res.data
}
})
},
handleSelect(op) {
this.$emit('select', op)
this.show = false
}
}
}
</script>
<style lang="scss" scoped>
.AiSearchPopup {
::v-deep .searchPane {
padding: 0 16px;
display: flex;
flex-direction: column;
height: 100%;
.title {
width: 100%;
height: 100px;
text-align: center;
line-height: 100px;
}
.result {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-bottom: 30px;
}
.option {
display: flex;
align-items: center;
height: 80px;
border-bottom: 1px solid #eee;
font-size: 32px;
}
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<section class="AiSelect">
<div class="display" v-if="$slots.default" @tap="handleShowOptions">
<slot/>
</div>
<div v-else class="display" @tap="handleShowOptions">
<div class="selectedLabel" v-if="selectedLabel">{{ selectedLabel }}</div>
<i v-else>{{ placeholder }}</i>
<u-icon name="arrow-right" color="#ddd"/>
</div>
<u-select v-model="show" :list="options" :mode="mode" @confirm="handleConfirm"/>
</section>
</template>
<script>
export default {
name: "AiSelect",
props: {
value: String,
placeholder: {default: "请选择"},
list: {default: () => []},
mode: {default: "single-column"},
dict: {default: ""},
disabled: Boolean
},
computed: {
selectedLabel() {
let label = this.options.find(e => e.value == this.value)?.label
return this.selected?.map(e => e.label)?.join(",") || label
},
options() {
return this.dict ? this.$dict.getDict(this.dict).map(e => ({
value: e.dictValue,
label: e.dictName
})) : this.list
}
},
data() {
return {
show: false,
selected: []
}
},
methods: {
handleConfirm(v) {
this.selected = v
this.$emit("data", this.selected)
this.$forceUpdate()
},
handleShowOptions() {
if (!this.disabled) this.show = true
}
}
}
</script>
<style lang="scss" scoped>
.AiSelect {
max-width: 100%;
::v-deep .u-icon {
margin-left: 8px;
}
.display {
display: flex;
align-items: center;
.selectedLabel {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
i {
font-style: normal;
color: $uni-text-color-grey;
}
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<div class="AiSelectEnterprise">
<tree :checkList="checkList" :props="prop" @sendValue="(val)=>checkList = val" :multiple="multiple" :isCheck="true"
:rootId="rootId"/>
<div class="footer">
<scroll-view scroll-x class="scroll" style="width: 100%;">
</scroll-view>
<div class="btn" @click="confirm">确定选择</div>
<AiBack :visible="true" eventName="update:visible" :data="false" @click.native="confirm"></AiBack>
</div>
</div>
</template>
<script>
import tree from "./tree";
import AiBack from "../AiBack";
export default {
name: "AiSelectEnterprise",
components: {tree, AiBack},
props: {
value: {
type: Array,
default: () => []
},
multiple: {
type: Boolean,
default: true
},
rootId: Object,
},
data() {
return {
tree: [],
checkList: this.value,
prop: {
label: 'name',
multiple: this.multiple,
},
map: {},
}
},
created() {
uni.pageScrollTo({
duration: 0,
scrollTop: 0
})
},
methods: {
confirm() {
let filter = []
this.map = {}
this.recursion(this.checkList)
Object.keys(this.map).map(e => filter.push(this.map[e]))
this.$emit("change", filter)
this.$emit('update:visible', false)
},
recursion(arr) {
if (arr?.length) {
arr.map(e => {
if ((e.type == 0 || e.openId) && e.checked && !this.map[e.id]) {
this.map[e.id] = e
this.recursion(e.childrenUser)
}
if (e.childrenDept?.length) {
this.recursion(e.childrenDept)
}
})
}
}
},
}
</script>
<style lang="scss" scoped>
.AiSelectEnterprise {
min-height: 100%;
background-color: #F5F5F5;
position: relative;
.footer {
width: 100%;
display: flex;
align-items: center;
z-index: 10;
background: #F4F8FB;
position: fixed;
left: 0;
bottom: 0;
box-sizing: border-box;
padding: 0 32px;
.scroll {
height: 118px;
::v-deep .uni-scroll-view-content {
display: flex;
align-items: center;
.tag {
width: 236px;
height: 72px;
background: #EAEEF1;
border-radius: 8px;
display: flex;
align-items: center;
margin-right: 16px;
& > img {
width: 48px;
height: 45px;
margin-right: 8px;
flex-shrink: 0;
}
& > label {
width: 148px;
height: 42px;
font-size: 30px;
font-weight: 600;
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.btn {
width: 192px;
height: 80px;
background: #1365DD;
border-radius: 4px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #FFFFFF;
margin-left: 8px;
}
}
}
</style>

View File

@@ -0,0 +1,481 @@
<template>
<div class="tree">
<ai-top-fixed>
<div class="top pad">
<u-search v-if="searchIf" placeholder="搜索" @change="confirmSearch" @search="confirmSearch" :clearabled="true"
v-model="keyword" :show-action="false" @clear="clear"></u-search>
<u-tabs :list="list" :current="current" item-width="50%" height="96" bar-width="192"
@change="tabChange"></u-tabs>
</div>
</ai-top-fixed>
<div class="tree-list">
<scroll-view scroll-x class="scroll pad" style="width:100%" :scroll-left="scrollLeft">
<div v-for="(item,index) in parent" class="inline-item" :key="index">
<div class="inline-item" v-if="index==0" @click.stop="backTree(item,-1)">
<text v-if="index==parent.length-1&&!isSear" class="none">可选范围</text>
<text v-else class="active">可选范围</text>
</div>
<div v-if="index==0 && isSear" @click.stop="backTree(item,-2)"
:class="[index==parent.length-1 && isSear] ? 'none inline-item':'active inline-item'">
<span style="margin: 0 8px">/</span>
搜索结果
</div>
<div class="inline-item" @click.stop="backTree(item,index)" v-if="index!=0">
<span style="margin: 0 8px">/</span>
<text v-if="index==parent.length-1" class="none inline-item">
{{item[tag]}}
</text>
<text v-else class="active">
{{item[tag]}}
</text>
</div>
</div>
</scroll-view>
<div class="container-list">
<div class="common" v-for="(item, index) in tree" @click.stop="toNext(item)" :key="index">
<label class="content">
<div class="checkbox" v-if="multiple" @click.stop="checkboxChange(item,index)">
<img :src="$cdn + 'common/xzh.png'" v-if="item.checked" alt="">
<img :src="$cdn + 'common/xzn.png'" v-else alt="">
</div>
<div class="checkbox" v-if="!multiple && (item.type==0 || item.openId)" @click.stop="checkbox(item,index)">
<img :src="$cdn + 'common/xzh.png'" v-if="item.checked" alt="">
<img :src="$cdn + 'common/xzn.png'" v-else alt="">
</div>
<div class="person" v-if="item.type==0">
<u-avatar :src="item.avatar || ($cdn + 'common/xztx.png')" mode="square" :size="74"></u-avatar>
</div>
<u-row justify="between" style="width: 100%;">
<div class="word" v-if="tag=='name'">
<img :src="$cdn + 'common/xzbq.png'" v-if="item.type==1" alt="">
<span class="ellipsis">{{item[tag]}}</span>
</div>
<div class="word" v-else-if="tag=='tagname'">
<template v-if="!item.openId">
<img :src="$cdn + 'common/xzbqbottom.png'" alt="">
<span class="ellipsis">{{item[tag]}}</span>
</template>
<template v-else>
<u-avatar :src="item.avatar || ($cdn + 'common/xztx.png')" mode="square" :size="74"
style="margin: 0 17px;"></u-avatar>
<span class="ellipsis">{{item["name"]}}</span>
</template>
</div>
<div class="right"
v-if="item.type==1 && (item.childrenDept.length || item.childrenUser.length) && tag=='name'"></div>
<div class="right" v-if="tag=='tagname' && !item.openId"></div>
</u-row>
</label>
</div>
</div>
</div>
</div>
</template>
<script>
import AiTopFixed from "../AiTopFixed";
export default {
name: "tree",
components: {AiTopFixed},
props: {
checkList: {
type: Array,
default: () => []
},
searchIf: {
type: Boolean,
default: () => true
},
multiple: {
type: Boolean,
default: true
},
rootId: Object,
},
data() {
return {
isSear: false,
tree: [],
parent: [1],
searchResult: [],
allData: [],
newCheckList: this.checkList,
scrollLeft: Infinity,
keyword: "",
current: 0,
tag: "name",
}
},
methods: {
tabChange(e) {
this.tag = e == 0 ? "name" : "tagname"
this.current = e
this.parent = [1]
// this.newCheckList = []
this.getTree()
},
clear() {
this.keyword = ""
this.tree = this.allData
this.parent = [1]
this.isSear = false
},
checkboxChange(item, index) {
if (item.checked) {
this.$set(this.tree[index], 'checked', false)
this.delChild(item)
for (let index = 0, n = this.newCheckList.length; index < n; index++) {
let temp = this.newCheckList[index];
if (temp.id == item.id) {
this.newCheckList.splice(index, 1)
break
}
}
} else {
(item.type == 0 || item.openId) && this.newCheckList.push(item)
this.$set(this.tree[index], 'checked', true)
this.chooseChild(item)
}
this.$emit('sendValue', this.newCheckList)
},
delUser(id) {
for (let i = 0, len = this.newCheckList.length; i < len; i++) {
if (this.newCheckList[i].id === id) {
return this.newCheckList.splice(i, 1)
}
}
},
chooseChild(arr) {
if (arr.childrenDept?.length) {
for (let i = 0, len = arr.childrenDept.length; i < len; i++) {
let item = arr.childrenDept[i]
item.checked = true
this.newCheckList.push(item)
this.chooseChild(item)
}
}
if (arr.childrenUser?.length) {
for (let i = 0, len = arr.childrenUser.length; i < len; i++) {
let item = arr.childrenUser[i]
item.checked = true
this.newCheckList.push(item)
this.chooseChild(item)
}
}
if (arr.users?.length) {
for (let i = 0, len = arr.users.length; i < len; i++) {
let item = arr.users[i]
item.checked = true
this.newCheckList.push(item)
this.chooseChild(item)
}
}
this.newCheckList = Array.from(new Set(this.newCheckList))
},
delChild(arr) {
if (arr.childrenDept?.length) {
for (let i = 0, len = arr.childrenDept.length; i < len; i++) {
let item = arr.childrenDept[i];
item.checked = false
for (let index = 0, n = this.newCheckList.length; index < n; index++) {
let temp = this.newCheckList[index];
if (temp.id == item.id) {
this.newCheckList.splice(index, 1)
break
}
}
this.delChild(item)
}
}
if (arr.childrenUser?.length) {
for (let i = 0, len = arr.childrenUser.length; i < len; i++) {
let item = arr.childrenUser[i];
item.checked = false
for (let index = 0, n = this.newCheckList.length; index < n; index++) {
let temp = this.newCheckList[index];
if (temp.id == item.id) {
this.newCheckList.splice(index, 1)
break
}
}
this.delChild(item)
}
}
},
//单选
checkbox(item, index) {
let status = !this.tree[index].checked
this.$set(this.tree[index], 'checked', status)
if (this.newCheckList.length <= 0) {
this.newCheckList = [this.tree[index]]
} else if (this.newCheckList.length == 1) {
this.tree.forEach(item => {
if (item.id != this.tree[index].id) {
item.checked = false
}
})
this.newCheckList = []
if (this.tree[index].checked) {
this.newCheckList.push(this.tree[index])
}
}
this.$emit('sendValue', this.newCheckList)
},
toNext(item) {
if (this.tag == "name") {
if (item.type == 1 && (item["childrenDept"].length || item["childrenUser"].length)) {
this.tree = []
if (item["childrenDept"].length) {
this.tree = item["childrenDept"]
}
if (item["childrenUser"].length) {
this.tree = [...this.tree, ...item["childrenUser"]]
}
this.checkIf()
if (this.parent[0].id !== item.id) {
this.parent.push(item)
}
}
} else if (this.tag == "tagname" && !item.openId) {
this.tree = item.users
if (this.parent[0].id !== item.id) {
this.parent.push(item)
}
}
this.$nextTick(() => {
this.scrollLeft += 200
})
},
checkIf() {
for (let i = 0, len = this.tree.length; i < len; i++) {
for (let j = 0, lens = this.newCheckList.length; j < lens; j++) {
if (this.newCheckList[j].id == this.tree[i].id) {
this.$set(this.tree[i], 'checked', true)
break
} else {
this.$set(this.tree[i], 'checked', false)
}
}
}
},
confirmSearch(val) {
this.searchResult = []
this.search(this.tree, val)
this.isSear = true
this.parent.splice(1, Infinity)
this.tree = this.searchResult
if(!val) this.clear()
},
search(data, keyword) {
if (data.length) {
for (let i = 0, len = data.length; i < len; i++) {
if (data[i].name?.indexOf(keyword) != -1) {
this.searchResult.push(data[i])
}
if (data[i]["childrenDept"]?.length || data[i]["childrenUser"]?.length) {
this.search(data[i]["childrenDept"].concat(data[i]["childrenUser"]), keyword)
}
if (data[i]["users"]?.length) {
this.search(data[i]["users"], keyword)
}
}
}
},
backTree(item, index) {
if (index == -1) {
this.tree = this.allData
this.parent.splice(1, Infinity)
this.isSear = false
this.keyword = ""
} else if (index == -2) {
this.tree = this.searchResult
this.parent.splice(1, Infinity)
} else {
if (this.parent.length - index > 2) {
this.parent.forEach((item, i) => {
if (i > index) {
this.parent.splice(i, Infinity)
}
})
} else if (index != this.parent.length - 1) {
this.parent.splice(this.parent.length - 1, 1)
}
this.tree = item["childrenDept"].concat(item["childrenUser"] || [])
}
if (this.multiple) return
this.checkIf()
},
getTree() {
this.$http.post(this.current == 0 ? "/app/wxcp/wxuser/tree" : "/app/wxcp/wxtag/tree", null, {
params: {
rootId: this.rootId
}
}).then(res => {
if (res && res.data) {
let result = this.tag == 'name' ? [res.data] : res.data
this.tree = result
this.allData = result
}
})
},
},
computed: {
list() {
return [
{name: "组织架构"},
{name: "标签"}
]
}
},
created() {
this.getTree()
}
}
</script>
<style lang="scss" scoped>
.tree {
min-height: 100%;
background-color: #F5F5F5;
.top {
background-color: #FFFFFF;
}
.tree-list {
margin-top: 24px;
background-color: #FFFFFF;
.scroll {
white-space: nowrap;
border-bottom: 1px solid #f4f4f4;
.inline-item {
height: 112px;
font-size: 30px;
display: inline-block;
line-height: 112px;
.active {
color: #4297ED !important;
font-weight: 600;
}
.none {
color: #666666;
font-weight: 600;
}
}
}
.container-list {
min-height: 1000px;
overflow-y: scroll;
overflow-x: hidden;
.common {
background-color: #fff;
border-bottom: 1px solid #f4f4f4;
box-sizing: border-box;
padding: 0 30px;
.content {
display: flex;
align-items: center;
height: 100px;
width: 100%;
line-height: 100px;
position: relative;
font-size: 32px;
.right {
width: 16px;
height: 16px;
border-right: 4px solid #CCCCCC;
border-top: 4px solid #CCCCCC;
transform: rotate(45deg);
}
.word {
display: flex;
align-items: center;
& > img {
width: 74px;
height: 74px;
margin: 0 34px;
}
.ellipsis{
width: 450px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.checkbox {
position: relative;
display: flex;
align-items: center;
& > img {
width: 48px;
height: 48px;
border-radius: 50%;
}
.color {
color: #00aaff;
background-color: #00aaff;
}
}
.person {
display: flex;
align-items: center;
color: #f57a00;
font-size: 36px;
text-align: center;
margin: 0 34px;
flex-shrink: 0;
}
}
}
}
}
::v-deep .content {
padding: 0 !important;
}
.pad {
box-sizing: border-box;
padding: 20px 32px 0;
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<section class="AiTabbar">
<div class="tabPane" v-for="(op,i) in tabbars" :key="i"
@click="$emit('update:active',i)">
<img :src="op.icon" alt=""/>
<span :class="{active:i==active}">{{ op.text }}</span>
</div>
</section>
</template>
<script>
export default {
name: "AiTabbar",
props: {
active: {default: 0},
list: {default: () => []},
},
computed: {
tabbars() {
return this.list.map((e, i) => ({
...e,
icon: i == this.active ? e.selectedIconPath : e.iconPath
}))
}
}
}
</script>
<style lang="scss" scoped>
.AiTabbar {
height: 98px;
width: 100%;
position: fixed;
bottom: 0;
background: #FFFFFF;
border-top: 1px solid #ddd;
display: flex;
z-index: 9;
.tabPane {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 22px;
cursor: pointer;
& > img {
height: 44px;
}
& > span {
color: #C4CAD4;
&.active {
color: #3267F0;
}
}
}
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<section class="AiTable">
<u-table color="#333">
<u-tr>
<u-th v-for="(col,i) in colConfigs" :key="i" :width="col.width">{{ col.label }}</u-th>
</u-tr>
<u-tr v-for="(row,j) in data" :key="j">
<u-td v-for="(col,i) in colConfigs" :key="i" :width="col.width">
<slot v-if="col.slot" :name="col.slot"/>
<p v-else-if="col.dict">{{ $dict.getLabel(col.dict, row[col.prop]) }}</p>
<p v-else>{{ row[col.prop] || "-" }}</p>
</u-td>
</u-tr>
</u-table>
</section>
</template>
<script>
import UTable from "../uview/components/u-table/u-table";
import UTd from "../uview/components/u-td/u-td";
import UTh from "../uview/components/u-th/u-th";
import UTr from "../uview/components/u-tr/u-tr";
export default {
name: "AiTable",
components: {UTr, UTh, UTd, UTable},
props: {
data: {default: () => []},
colConfigs: {default: () => []},
}
}
</script>
<style lang="scss" scoped>
.AiTable {
border-radius: 8px;
min-height: 100px;
overflow: hidden;
.u-table, .u-th {
border-color: #D0D4DC !important;
}
.u-th {
background-color: #DFE6F4;
color: #646D7F;
}
.u-tr {
height: 80px;
}
}
</style>

81
src/components/AiTabs.vue Normal file
View File

@@ -0,0 +1,81 @@
<template>
<section class="AiTabs" :class="{wrap}">
<div class="tabItem" v-for="(op,i) in ops" :key="i"
:class="{active:value==op.value,plain}"
:style="{width:itemWidth}"
@tap="$emit('change',op.value)">
{{ op.name }}
</div>
<div class="end">
<slot name="end"/>
</div>
</section>
</template>
<script>
export default {
name: "AiTabs",
model: {
prop: "value",
event: "change"
},
props: {
value: {default: ""},
ops: {default: () => []},
wrap: Boolean,
plain: Boolean,
itemWidth: String
},
}
</script>
<style lang="scss" scoped>
.AiTabs {
display: flex;
flex: 1;
min-width: 0;
max-height: 240px;
overflow-y: auto;
&.wrap {
flex-wrap: wrap;
}
.tabItem {
flex-shrink: 0;
min-width: 144px;
max-width: 100%;
min-height: 64px;
font-size: 28px;
font-weight: 400;
color: #666;
background: #FFFFFF;
border-radius: 4px;
border: 1px solid #CCC;
text-align: center;
line-height: 64px;
margin-bottom: 16px;
margin-right: 16px;
padding: 0 16px;
box-sizing: border-box;
&.active {
border-color: $uni-color-primary;
color: $uni-color-primary;
&.plain {
color: #fff;
border-color: transparent;
background: $uni-color-primary;
}
}
}
.end {
flex: 1;
min-width: 0;
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<section class="AiTextarea" :class="{border}">
<u-input type="textarea" v-bind="$attrs" :value="value" :maxlength="maxlength"
@input="handleInput" :disabled="disabled"/>
<div class="bottomBar">
<div class="leftPane">
<slot name="bar"/>
</div>
<div v-if="!!maxlength">{{ value.length }}/{{ maxlength }}</div>
</div>
</section>
</template>
<script>
import UInput from "../uview/components/u-input/u-input";
export default {
name: "AiTextarea",
components: {UInput},
model: {
prop: "value",
event: "change"
},
props: {
value: {default: ""},
maxlength: {default: 0},
border: Boolean,
disabled: Boolean
},
methods: {
handleInput(v) {
this.$emit('change', v)
}
}
}
</script>
<style lang="scss" scoped>
.AiTextarea {
width: 100%;
position: relative;
&.border {
::v-deep textarea {
border-radius: 4px;
border: 1px solid #e2e1e1;
padding: 16px 16px 36px;
box-sizing: border-box;
}
.bottomBar {
position: absolute;
bottom: 8px;
right: 16px;
}
::v-deep .u-input__right-icon {
position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
}
}
.bottomBar {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 36px;
color: #999999;
.leftPane {
display: flex;
& > * + * {
margin-left: 32px;
}
}
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<section class="AiTopFixed" :style="{background}">
<!--占位区-->
<div class="placeholder">
<div v-if="$slots.tabs">
<slot name="tabs"/>
</div>
<div class="content" v-if="$slots.default">
<slot/>
</div>
</div>
<!--悬浮区-->
<div class="fixed" :style="{background}">
<div v-if="$slots.tabs">
<slot name="tabs"/>
</div>
<div class="content" v-if="$slots.default">
<slot/>
</div>
</div>
</section>
</template>
<script>
export default {
name: "AiTopFixed",
props: {
background: {default: "#fff"}
}
}
</script>
<style lang="scss" scoped>
.AiTopFixed {
width: 100%;
& > div {
width: 100%;
}
.fixed {
width: 100%;
top: 0;
position: fixed;
z-index: 9;
}
.placeholder {
visibility: hidden;
opacity: 0;
}
.content {
padding: 20px 32px;
box-sizing: border-box;
}
::v-deep .u-search {
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
margin-bottom: 32px !important;
}
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<div class="ai-uploader">
<div class="fileList">
<div class="item" v-for="(item, i) in fileList" :key="i">
<template v-if="type == 'image'">
<ai-image :src="item.url" :preview="preview"/>
<div class="info">
<i>{{ item.fileSizeStr }}</i>
</div>
</template>
<template v-else>
<ai-image :preview="preview" :file="item"/>
<div class="info">
<span>{{ item.name }} </span>
<i>{{ item.fileSizeStr }}</i>
</div>
</template>
<template v-if="!disabled">
<div btn @tap="handleReUpload(i)">
重新上传
</div>
<div btn @tap="remove(i)">
删除
</div>
</template>
</div>
<div v-if="!disabled&&(fileList.length == 0 || (multiple && fileList.length < limit))" class="default"
@click="upload">
<i class="iconfont iconfont-iconAdd"/>
<span>{{ placeholder }}</span>
</div>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
import AiImage from './AiImage'
export default {
name: 'AiUploader',
components: {AiImage},
props: {
limit: {default: 1}, //数量
placeholder: {default: '添加图片'}, // 文字提示
type: {default: 'image'}, // 文件类型image还是file
multiple: {
type: Boolean,
default: false,
},
fileId: String,
mediaId: String,
def: {default: () => []},
action: {default: '/app/wxcp/upload/uploadFile'},
preview: Boolean,
size: {default: 0},
disabled: Boolean
},
computed: {
...mapState(['baseURL', 'token']),
errorImage() {
return this.$cdn + 'file.png'
},
},
watch: {
def: {
handler(v) {
if (!!v?.toString() && v?.url) {
if (this.multiple) {
this.fileList = v
} else {
this.fileList = [v]
}
}
},
immediate: true,
},
},
data() {
return {
fileList: [],
}
},
methods: {
remove(index) {
this.fileList.splice(index, 1)
this.$emit('list', this.fileList)
},
upload(wait) {
let params = {
count: this.limit,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
let count = this.fileList?.length + (res.tempFiles?.length || res.tempFile ? 1 : 0)
if (count > this.limit && this.limit !== 1) {
return this.$u.toast(`不能超过${this.limit}`)
}
if (res.tempFiles) {
res.tempFiles?.map((item) => {
this.uploadFile(item)
})
} else if (res?.tempFile) {
this.uploadFile(res.tempFile)
}
},
}
typeof wait == 'function' && wait()
if (this.type == 'image') {
uni.chooseImage(params)
} else if (this.type == 'video') {
uni.chooseVideo(params)
} else {
uni.chooseFile(params)
}
},
uploadFile(img) {
if (this.size > 0 && img.size > this.size) {
return this.$u.toast(`不能超过${Math.ceil(this.size / 1024 / 1024)}MB`)
}
uni.showLoading({title: '上传中'})
let formData = new FormData()
formData.append('file', img)
this.$http
.post(this.action, formData, {
params: {type: this.type},
})
.then((res) => {
uni.hideLoading()
if (res?.data) {
this.$emit('data', res.data)
this.$u.toast('上传成功!')
if (this.action == '/app/wxcp/upload/uploadFile') {
this.$emit('update:mediaId', res.data?.media?.mediaId)
this.$emit('update:fileId', res.data.file.id)
this.fileList.push(res.data.file)
} else if (this.action == '/admin/file/add2') {
let info = res.data
this.$emit('update:fileId', info?.id)
this.fileList.push(res.data)
}
this.$emit("update:def", this.fileList)
this.$emit("list", this.fileList)
} else {
this.$u.toast(res.msg)
}
})
.catch(() => uni.hideLoading())
},
handleReUpload(i) {
this.upload(() => this.remove(i))
},
},
}
</script>
<style lang="scss" scoped>
.ai-uploader {
width: 100%;
line-height: normal;
margin-bottom: 16px;
.fileList {
.item {
display: flex;
align-items: center;
margin-bottom: 10px;
image {
width: 160px;
height: 160px;
}
i {
font-style: normal;
color: #9b9b9b;
}
.info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
& > span {
overflow: hidden;
text-overflow: ellipsis;
}
}
div[btn] {
color: $uni-color-primary;
}
div:nth-child(4) {
color: #f72c27;
}
& > * + * {
margin-left: 20px;
}
}
.default {
width: 240px;
height: 240px;
box-sizing: border-box;
border-radius: 8px;
background: #f3f4f7;
color: #89b;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.iconfont-iconAdd {
font-size: 64px;
}
span {
display: block;
text-align: center;
font-size: 28px;
}
}
}
}
</style>

185
src/components/AiVideo.vue Normal file
View File

@@ -0,0 +1,185 @@
<template>
<view class="imt-audio">
<view class="audio-wrapper">
<view class="audio-number">{{format(current)}}</view>
<slider class="audio-slider" :activeColor="color" block-size="16" :value="current" :max="duration || 10" @changing="seek=true,current=$event.detail.value" @change="audio.seek($event.detail.value)"></slider>
<view class="audio-number">{{format(duration)}}</view>
</view>
<view class="audio-control-wrapper" :style="{color}">
<image
class="audio-control audio-control-switch"
@click="audio.paused?play():audio.pause()"
:src="paused ? playImg : stopImg" />
<p>{{ paused ? '点击播放' : '点击停止播放' }}</p>
</view>
</view>
</template>
<script>
import stopImg from '../pages/resourcesManage/img/stop-img.png'
import playImg from '../pages/resourcesManage/img/play-icon.png'
export default {
data() {
return {
audio: uni.createInnerAudioContext(),
current: 0, //当前进度(s)
duration: 0, //总时长(s)
paused: true, //是否处于暂停状态
loading: false, //是否处于读取状态
seek: false,
stopImg,
playImg
}
},
props: {
src: String, //音频链接
autoplay: Boolean, //是否自动播放
continue: Boolean, //播放完成后是否继续播放下一首,需定义@next事件
control: {
type: Boolean,
default: true
}, //是否需要上一曲/下一曲按钮
color: {
type: String,
default: '#007BFF'
} //主色调
},
methods: {
//返回prev事件
prev() {
this.$emit('prev')
},
//返回next事件
next() {
this.$emit('next')
},
//格式化时长
format(num) {
return '0'.repeat(2 - String(Math.floor(num / 60)).length) + Math.floor(num / 60) + ':' + '0'.repeat(2 - String(Math.floor(num % 60)).length) + Math.floor(num % 60)
},
//点击播放按钮
play() {
this.audio.play()
this.loading = true
}
},
created() {
if (this.src) {
this.audio.src = this.src
this.autoplay && this.play()
}
this.audio.obeyMuteSwitch = false
//音频进度更新事件
this.audio.onTimeUpdate(() => {
if (!this.seek) {
this.current = this.audio.currentTime
}
if (!this.duration) {
this.duration = this.audio.duration
}
})
//音频播放事件
this.audio.onPlay(() => {
this.paused = false
this.loading = false
})
//音频暂停事件
this.audio.onPause(() => {
this.paused = true
})
//音频结束事件
this.audio.onEnded(() => {
if (this.continue) {
this.next()
} else {
this.paused = true
this.current = 0
}
})
//音频完成更改进度事件
this.audio.onSeeked(() => {
this.seek = false
})
},
beforeDestroy(){
this.audio.destroy()
},
watch: {
src(src, old) {
this.audio.src = src
this.current = 0
this.duration = 0
if (old || this.autoplay) {
this.play()
}
}
}
}
</script>
<style>
.imt-audio {
background: #fff;
border-radius: 20upx;
}
.audio-wrapper {
display: flex;
align-items: center;
}
.audio-number {
width: 120upx;
font-size: 24upx;
line-height: 1;
color: #999999;
text-align: center;
}
.audio-slider {
flex: 1;
margin: 0;
}
.audio-control-wrapper {
margin-top: 40upx;
text-align: center;
}
.audio-control-wrapper p {
color: #999999;
font-size: 26rpx;
}
.audio-control-wrapper image {
width: 128rpx;
height: 128rpx;
}
.audio-control {
font-size: 32upx;
line-height: 1;
border-radius: 50%;
}
.audio-control-next {
transform: rotate(180deg);
}
.audio-control-switch {
font-size: 40upx;
margin: 0 100upx;
}
.audioLoading {
animation: loading 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes loading {
to {
transform: rotate(360deg);
}
}
</style>

29
src/components/VDrag.vue Normal file
View File

@@ -0,0 +1,29 @@
<template>
<section class="VDrag">
<vuedraggable v-bind="$attrs" @change="handleChange">
<slot/>
</vuedraggable>
</section>
</template>
<script>
import vuedraggable from 'vuedraggable'
export default {
name: "VDrag",
components: {vuedraggable},
data: () => ({
moveEvt: null
}),
methods: {
handleChange(moved) {
this.$emit('move', moved)
}
}
}
</script>
<style lang="scss" scoped>
.VDrag {
}
</style>