初始化产品库
This commit is contained in:
69
src/components/AiAdd.vue
Normal file
69
src/components/AiAdd.vue
Normal 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
87
src/components/AiBack.vue
Normal 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
115
src/components/AiCard.vue
Normal 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
76
src/components/AiCell.vue
Normal 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
60
src/components/AiDate.vue
Normal 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>
|
||||
39
src/components/AiEmpty/AiEmpty.vue
Normal file
39
src/components/AiEmpty/AiEmpty.vue
Normal 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>
|
||||
BIN
src/components/AiEmpty/static/1/Icon/Fields/bianhao@2x.png
Normal file
BIN
src/components/AiEmpty/static/1/Icon/Fields/bianhao@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 806 B |
BIN
src/components/AiEmpty/static/1/Icon/Fields/time@2x.png
Normal file
BIN
src/components/AiEmpty/static/1/Icon/Fields/time@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/components/AiEmpty/static/Empty.png
Normal file
BIN
src/components/AiEmpty/static/Empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
36
src/components/AiFixedBtn.vue
Normal file
36
src/components/AiFixedBtn.vue
Normal 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>
|
||||
72
src/components/AiImage.vue
Normal file
72
src/components/AiImage.vue
Normal 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>
|
||||
28
src/components/AiLoading.vue
Normal file
28
src/components/AiLoading.vue
Normal 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
68
src/components/AiMap.vue
Normal 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>
|
||||
85
src/components/AiResult.vue
Normal file
85
src/components/AiResult.vue
Normal 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>
|
||||
87
src/components/AiSearchPopup.vue
Normal file
87
src/components/AiSearchPopup.vue
Normal 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>
|
||||
83
src/components/AiSelect.vue
Normal file
83
src/components/AiSelect.vue
Normal 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>
|
||||
148
src/components/AiSelectEnterprise/AiSelectEnterprise.vue
Normal file
148
src/components/AiSelectEnterprise/AiSelectEnterprise.vue
Normal 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>
|
||||
481
src/components/AiSelectEnterprise/tree.vue
Normal file
481
src/components/AiSelectEnterprise/tree.vue
Normal 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>
|
||||
63
src/components/AiTabbar.vue
Normal file
63
src/components/AiTabbar.vue
Normal 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>
|
||||
53
src/components/AiTable.vue
Normal file
53
src/components/AiTable.vue
Normal 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
81
src/components/AiTabs.vue
Normal 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>
|
||||
82
src/components/AiTextarea.vue
Normal file
82
src/components/AiTextarea.vue
Normal 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>
|
||||
63
src/components/AiTopFixed.vue
Normal file
63
src/components/AiTopFixed.vue
Normal 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>
|
||||
229
src/components/AiUploader.vue
Normal file
229
src/components/AiUploader.vue
Normal 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
185
src/components/AiVideo.vue
Normal 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
29
src/components/VDrag.vue
Normal 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>
|
||||
Reference in New Issue
Block a user