初始化产品库

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

174
src/pages/app.vue Normal file
View File

@@ -0,0 +1,174 @@
<template>
<section class="app">
<u-swiper :list="list" height="240" bg-color="#408CFF"></u-swiper>
<label class="useful-app">常用应用</label>
<u-grid
:col="o.length"
:border="false"
v-for="(o, i) in gridGroup"
:key="i"
>
<u-grid-item v-for="(i, j) in o" :key="j" @click="linkTo(i)">
<u-icon :name="i.icon" :size="100"></u-icon>
<view class="grid-text">{{ i.label }}</view>
</u-grid-item>
</u-grid>
<label class="useful-app notice">通知公告</label>
<section class="list">
<div class="item" v-for="(item, index) in dataList" :key="index">
<label class="title hidden">{{ item.title }}</label>
<div class="content hidden">{{ item.content }}</div>
<div class="date">{{ item.createTime }}</div>
</div>
<u-loadmore :status="status" />
</section>
</section>
</template>
<script>
export default {
name: 'app',
data() {
return {
list: [this.imgHomeUrl + 'banner.png'],
gridGroup: [
[
{
icon: `${this.imgHomeUrl}icon1.png`,
label: '随心问',
link: '/pages/casuallyask/casuallyask'
},
{
icon: `${this.imgHomeUrl}icon2.png`,
label: '随手拍',
link: '/pages/snapshot/snapshot'
},
{
icon: `${this.imgHomeUrl}icon3.png`,
label: '网上办事',
link: '/pages/workonline/workonline'
},
{ icon: `${this.imgHomeUrl}icon4.png`, label: '调查走访' }
],
[
{
icon: `${this.imgHomeUrl}icon5.png`,
label: '信用好超市',
link: '/pages/supermarket/supermarket'
},
{ icon: `${this.imgHomeUrl}icon6.png`, label: '中心工作' },
{ icon: `${this.imgHomeUrl}icon7.png`, label: '会务通知' },
{ icon: `${this.imgHomeUrl}icon8.png`, label: '工作去向' },
{ icon: `${this.imgHomeUrl}icon8.png`, label: '大喇叭' }
]
],
status: 'nomore',
dataList: [],
current: 0
}
},
onLoad() {
this.getList()
},
methods: {
getList() {
if (this.loadingStatus == 'nomore') return
this.$http
.post(`/app/appworkcenternotice/list`, null, {
params: {
withoutToken: true,
current: this.current + 1,
size: 10
}
})
.then(res => {
if (res && res.data) {
this.dataList = res.data.records
}
})
},
linkTo(val) {
if (val.link) {
uni.navigateTo({ url: val.link })
}
}
},
onReachBottom() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.app {
min-height: 100%;
background-color: #ffffff;
box-sizing: border-box;
padding: 32px;
position: relative;
.grid-text {
font-size: 28px;
color: #333333;
line-height: 46px;
}
.useful-app {
margin-top: 80px;
display: inherit;
font-size: 36px;
font-weight: bold;
color: #333333;
line-height: 50px;
}
.notice {
margin-top: 56px;
}
.list {
margin-top: 38px;
.item {
height: 192px;
background: #f7f9ff;
border-radius: 8px;
box-sizing: border-box;
padding: 20px;
margin-bottom: 32px;
.title {
font-size: 32px;
font-weight: 800;
color: #343d65;
}
.content {
font-size: 28px;
color: #333333;
font-weight: 500;
margin: 20px 0 8px 0;
}
.date {
font-size: 28px;
color: #666666;
}
}
}
.hidden {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
::v-deep .u-indicator-item-round {
background-color: #d8dde6;
}
::v-deep .u-indicator-item-round-active {
background-color: #408cff !important;
}
}
</style>

View File

@@ -0,0 +1,700 @@
<template>
<div class="add-form" v-if="pageShow">
<div class="header-pic">
<image v-if="form.headPicture" :src="form.headPicture" />
<span @click="upload">更换图片</span>
</div>
<div class="form-info">
<h2>文本选项</h2>
<div class="form-info__wrapper">
<textarea class="title" placeholder="请输入标题 (必填)" :maxlength="30" :auto-height="true" v-model="form.title"></textarea>
<u-input class="content" :clearable="false" type="textarea" v-model="form.tableExplain" placeholder="请输入表单描述 (选填)" :height="80" :auto-height="true" :maxlength="255"></u-input>
</div>
</div>
<draggable
class="components-list"
v-model="targetList"
:animation="340"
scroll
element="div"
:options="{
animation: 340,
handle: '.components-item__title'
}"
draggable=".components-item"
:sort="true">
<div class="components-item" v-for="(item, index) in targetList" :key="index" @click="toFiledSetting(item, index)">
<div class="components-item__title">
<div class="components-item__title--left">
<em :style="{opacity: item.required ? 1 : 0}">*</em>
<i>{{ index + 1 }}.</i>
<h2>{{ item.label }}</h2>
</div>
<image :src="`${$cdn}askform/sc1.png`" @click.stop="removeComponent(index)" @touchstart.stop="removeComponent(index)" />
</div>
<div class="components-item__filed">
<template v-if="(item.type === 'radio')">
<u-radio-group v-model="item.value" wrap>
<u-radio disabled :name="field.label" v-for="(field, i) in item.options" :key="i">
<image :src="field.img[0].url" v-if="field.img.length"/>
<span>{{ field.label }}</span>
</u-radio>
</u-radio-group>
</template>
<template v-if="(item.type === 'checkbox')">
<u-checkbox-group v-model="item.value" wrap>
<u-checkbox disabled :name="field.label" v-for="(field, i) in item.options" :key="i">
<image :src="field.img[0].url" v-if="field.img.length"/>
<span>{{ field.label }}</span>
</u-checkbox>
</u-checkbox-group>
</template>
<template v-if="(item.type === 'select')">
<div class="components-item__select">
<span>{{ item.placeholder }}</span>
<u-icon name="arrow-down" color="#DEDFDF" />
</div>
</template>
<template v-if="(item.type === 'upload')">
<div class="components-item__select components-item__textarea components-item__upload">
<image :src="`${$cdn}askform/upload.png`" />
<span>选择图片2M以内</span>
</div>
</template>
<template v-if="(item.type === 'input')">
<div class="components-item__select">
<span>{{ item.placeholder }}</span>
</div>
</template>
<template v-if="(item.type === 'textarea')">
<div class="components-item__select components-item__textarea">
<span>{{ item.placeholder }}</span>
</div>
</template>
</div>
</div>
</draggable>
<div class="add-form__btn" @click="isShow = true">
<image :src="`${$cdn}askform/add.png`" />
<span>添加问题</span>
</div>
<div class="add-form__footer">
<div>
<span @click="toPreview">预览</span>
<span @click="toSetting">设置</span>
</div>
<div @click="onConfirm">立即发布</div>
</div>
<u-popup v-model="isShow" :closeable="false" mode="bottom">
<div class="add-popup">
<div class="add-popup__title">
<h2>添加问题</h2>
<image :src="`${$cdn}askform/zk.png`" mode="aspectFit" @click="isShow = false" />
</div>
<div class="add-popup__list">
<span @click="toFiledSetting('radio')">单选题</span>
<span @click="toFiledSetting('checkbox')">多选题</span>
<span @click="toFiledSetting('select')">单下拉框</span>
<span @click="toFiledSetting('input')">单行填空</span>
<span @click="toFiledSetting('textarea')">多行填空</span>
<span @click="toFiledSetting('upload')">上传图片</span>
</div>
</div>
</u-popup>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
import draggable from 'vuedraggable'
export default {
data () {
return {
pageShow: false,
form: {
tableExplain: '详细描述',
title: '问卷调查',
isShowheadPicture: true,
isShowTableExplain: true,
isShowBtn: true,
headPicture: '',
commitType: '1',
periodValidityType: '0',
actionNotice: '1',
dynamicNotice: '1',
periodValidityEndTime: '',
shareStatus: '0',
count: 0,
wechatId: '0',
type: 0,
buttonExplain: '提交',
tips: true
},
templateType: 0,
targetList: [],
isShow: false,
type: 0,
id: '',
isQuote: false,
touchStart: 0,
formConfig: {}
}
},
components: {
AiBack,
draggable
},
onLoad (query) {
this.type = Number(query.type)
if (query.isQuote) {
this.isQuote = true
}
if (query.id) {
this.id = query.id
this.getInfo(query.id)
} else {
this.pageShow = true
}
this.init()
uni.$on('setting', res => {
this.form = {
...this.form,
...res
}
this.formConfig = res
})
uni.$on('filedConfig', res => {
if (res.index === '-1') {
this.targetList.push(res.config)
} else {
this.$set(this.targetList, [res.index], res.config)
}
})
},
methods: {
toSetting () {
uni.navigateTo({
url: `/pages/askForm/formSetting?id=${this.id}&formConfig=${JSON.stringify(this.formConfig)}`
})
},
removeComponent (index) {
this.targetList.splice(index, 1)
},
toPreview () {
uni.navigateTo({
url: `/pages/askForm/previewForm?targetList=${JSON.stringify(this.targetList)}&form=${JSON.stringify(this.form)}`
})
},
upload() {
let params = {
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
let count = this.fileList?.length + (res.tempFiles?.length || res.tempFile ? 1 : 0)
if (count > 1) {
return this.$u.toast(`不能超过1个`)
}
if (res.tempFiles) {
res.tempFiles.map((item) => {
this.uploadFile(item)
})
} else if (res?.tempFile) {
this.uploadFile(res.tempFile)
}
}
}
uni.chooseImage(params)
},
uploadFile (img) {
uni.showLoading({title: '上传中'})
let formData = new FormData()
formData.append('file', img)
this.$http.post('/admin/file/add2', formData).then((res) => {
uni.hideLoading()
if (res?.data) {
this.$u.toast('上传成功!')
this.form.headPicture = res.data.url
}
}).catch(res => {
this.$u.toast(res)
uni.hideLoading()
})
},
onConfirm () {
for (let item of this.targetList) {
if (item.isShowPoints) {
if (item.pointType === '0') {
if (!item.answer || JSON.stringify(item.answer) === '[]') {
return this.$u.toast(`请输入${item.label}正确答案`)
}
if (!item.points) {
return this.$u.toast(`请输入${item.label}的分值`)
}
}
if (item.pointType === '1') {
for (let option of item.options) {
if (!option.point) {
return this.$u.toast(`请输入${item.label}${option.label}的分值`)
}
}
}
if (item.pointType === '2') {
for (let option of item.options) {
if (!option.point) {
return this.$u.toast(`请输入${item.label}${option.label}的分值`)
}
}
if (!item.points) {
return this.$u.toast(`请输入${item.label}全部答对分值`)
}
}
}
}
const fields = this.targetList.map(item => {
return {
fieldType: item.type,
fieldName: item.label,
fieldInfo: JSON.stringify(item)
}
})
this.$http.post(`/app/appquestionnairetemplate/addOrUpdate`, {
...this.form,
fields,
status: 1,
id: this.isQuote ? '' : this.id,
headPicture: this.form.headPicture,
type: this.type,
templateType: 0
}).then(res => {
if (res.code == 0) {
this.$u.toast('提交成功')
setTimeout(() => {
uni.$emit('reload')
uni.navigateBack({
delta: 1
})
}, 600)
}
}).catch(e => {
this.$u.toast(e)
})
},
getInfo (id) {
uni.showLoading()
this.$http.post(`/app/appquestionnairetemplate/queryDetailById?id=${id}`).then(res => {
if (res.code == 0) {
this.form = {
...res.data,
headPicture: res.data.headPicture
}
this.type = res.data.type
this.targetList = res.data.fields.map(item => {
return JSON.parse(item.fieldInfo)
})
this.pageShow = true
} else {
this.$u.toast(res.msg)
}
uni.hideLoading()
}).catch(() => {
uni.hideLoading()
})
},
toFiledSetting (type, index) {
if (index != undefined) {
uni.navigateTo({
url: `/pages/askForm/filedConfig?config=${JSON.stringify(type)}&index=${index}`
})
return false
}
this.isShow = false
uni.navigateTo({
url: `/pages/askForm/filedConfig?type=${type}`
})
},
init () {
if (this.type == 0) {
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/interview.png'
}
if (this.type == 1) {
this.form.title = '考试测评'
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/exam.png'
}
if (this.type == 2) {
this.form.title = '报名登记'
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/apply.png'
}
if (this.type == 3) {
this.form.title = '满意调查'
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/satisfaction.png'
}
if (this.type == 4) {
this.form.title = '投票评选'
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/vote.png'
}
}
}
}
</script>
<style lang="scss" scoped>
.add-form {
min-height: 100vh;
padding-bottom: 140px;
box-sizing: border-box;
background: #F3F6F9;
* {
box-sizing: border-box;
}
::v-deep .u-drawer-bottom {
background-color: transparent;
}
.components-list {
padding: 0 20px;
.components-item {
margin-top: 24px;
padding: 32px;
box-shadow: 0 4px 8px 4px rgba(233, 233, 233, 0.39);
border-radius: 8px;
overflow: hidden;
border: 1px solid #EEEFF0;
background: #fff;
::v-deep .u-radio, ::v-deep .u-checkbox {
position: relative;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.u-checkbox__icon-wrap, .u-radio__icon-wrap {
position: absolute;
top: 4rpx;
}
.u-radio__label, .u-checkbox__label {
display: flex;
align-items: center;
margin-right: 0;
margin-left: 40px;
text-align: justify;
span {
line-height: 1.5;
}
image {
width: 100px;
height: 100px;
margin: 0 10px;
}
}
}
span {
flex: 1;
color: #666;
font-size: 26px;
}
.components-item__select {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 80px;
margin-bottom: 8px;
padding: 0 26px;
border: 1px solid #DEDFDF;
&.components-item__textarea {
align-items: flex-start;
height: 160px;
padding-top: 20px;
image {
width: 46px;
height: 34px;
margin-right: 16px;
}
span {
color: #666;
font-size: 26px;
}
}
&.components-item__upload {
justify-content: center;
align-items: center;
}
}
.components-item__title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
em {
margin-right: 4px;
font-style: normal;
color: rgb(226, 33, 32);;
}
image {
position: relative;
flex-shrink: 1;
right: -20px;
width: 32px;
height: 32px;
box-sizing: content-box;
padding: 30px 20px 30px 20px;
}
div {
display: flex;
align-items: baseline;
max-width: 550px;
color: #333333;
font-size: 32px;
i {
font-style: normal;
}
h2 {
font-weight: 600;
font-size: 32px;
}
}
}
}
}
.add-popup {
height: 440px;
border-radius: 20px 20px 0 0;
background: #fff;
.add-popup__title {
display: flex;
position: relative;
align-items: center;
justify-content: center;
height: 96px;
border-bottom: 1px solid #E4E5E6;
h2 {
color: #333333;
font-size: 32px;
font-weight: 600;
}
image {
position: absolute;
right: 32px;
top: 50%;
width: 30px;
height: 20px;
transform: translateY(-50%);
}
}
.add-popup__list {
display: flex;
flex-wrap: wrap;
padding: 0 34px;
span {
width: calc((100% - 64px) / 3);
height: 78px;
line-height: 78px;
margin-top: 32px;
margin-right: 32px;
text-align: center;
color: #333333;
font-size: 28px;
border-radius: 8px;
border: 1px solid #E4E5E6;
&:nth-of-type(3n) {
margin-right: 0;
}
}
}
}
.add-form__footer {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
width: 100%;
height: 112px;
text-align: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-align: center;
background: #fff;
&:last-child {
color: #fff;
font-size: 36px;
background: #3192F4;
&:active {
opacity: 0.8;
}
}
span {
flex: 1;
height: 100%;
line-height: 112px;
color: #333333;
font-size: 32px;
&:active {
background: #eee;
}
}
}
}
.add-form__btn {
display: flex;
align-items: center;
justify-content: center;
width: 214px;
height: 66px;
line-height: 66px;
margin: 64px auto 0;
background: #FFFFFF;
border-radius: 34px;
&:active {
opacity: 0.8;
}
image {
width: 28px;
height: 28px;
}
span {
margin-left: 16px;
color: #4392E6;
font-size: 28px;
}
}
* {
box-sizing: border-box;
}
.form-info {
padding: 0 20px;
& > h2 {
height: 76px;
line-height: 76px;
color: #999999;
font-weight: normal;
font-size: 28px;
}
.form-info__wrapper {
padding: 0 18px;
background: #fff;
.title {
width: 100%;
padding: 22px 0;
font-size: 36px;
border-bottom: 1px solid #F1F2F3;
}
.content {
padding: 30px 0!important;
font-size: 28px;
::v-deep textarea {
color: #333;
font-size: 28px!important;
}
}
}
}
.header-pic {
position: relative;
font-size: 0;
span {
position: absolute;
bottom: 16px;
right: 16px;
z-index: 1;
width: 148px;
height: 56px;
line-height: 56px;
text-align: center;
color: #fff;
font-size: 26px;
background: rgba(0, 0, 0, 0.16);
border-radius: 28px;
}
image {
width: 100%;
height: 320px;
}
}
::v-deep .u-radio, ::v-deep .u-checkbox {
align-items: baseline;
}
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<section class="askForm">
<template v-if="showDetail&&!isManager">
<form-detail/>
</template>
<template v-else-if="isManager">
<form-list ref="FormList"/>
</template>
<ai-loading v-else :tips="errMsg"/>
</section>
</template>
<script>
import AiLoading from "../../components/AiLoading";
import {mapState} from "vuex";
import FormDetail from "./formDetail";
import FormList from "./formList";
export default {
name: "askForm",
components: {FormList, FormDetail, AiLoading},
computed: {
...mapState(['openUser', 'user']),
showDetail() {
return !!this.$route.query?.id
},
isManager() {
let {hash, query: {preview}} = this.$route
if (preview) return false
else if (hash == "#dev") return true
else return hash != '#form' && !!this.user.id
}
},
data() {
return {
errMsg: "加载表单中..."
}
},
onReachBottom() {
this.$refs?.FormList?.reachBottom()
}
}
</script>
<style lang="scss" scoped>
.askForm {
position: absolute;
width: 100%;
height: 100%;
background: #fff;
}
</style>

View File

@@ -0,0 +1,182 @@
<template>
<div class="form">
<div class="form-list">
<div
class="form-list__item"
@click="toAdd(`/pages/askForm/addForm?type=${index}`)"
:style="{'background-image': `url(${$cdn}askform/${index + 1}.png)`}"
v-for="(item, index) in itemList"
:key="index">
<h2>{{ item.name }}</h2>
<div>立即创建</div>
</div>
</div>
<div class="template" v-if="list.length">
<h2>共享模板</h2>
<div class="template-list">
<div class="template-item" v-for="(item, index) in list" :key="index" hover-class="bg-hover" @click="quote(item.id)">
<image :src="`${$cdn}askform/6.png`" />
<h2>{{ item.title }}</h2>
<u-icon name="arrow-right" color="#E1E2E3" />
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'addList',
label: '新建项目',
data () {
return {
itemList: [{
name: '调查问卷'
}, {
name: '考试测评'
}, {
name: '报名登记'
}, {
name: '满意调查'
}, {
name: '投票评选'
}],
list: []
}
},
mounted () {
this.getList()
},
methods: {
toAdd (url) {
uni.navigateTo({
url
})
},
quote (id) {
uni.navigateTo({
url: `/pages/askForm/addForm?isQuote=1&id=${id}`
})
},
getList () {
this.$http.post(`/app/appquestionnairetemplate/list`, null, {
params: {
current: 1,
templateType: 1,
size: 10000
}
}).then(res => {
if (res.code == 0) {
this.list = res.data.records
}
})
}
}
}
</script>
<style lang="scss" scoped>
.template {
margin: 32px 32px 0;
background: #fff;
border-radius: 8px;
overflow: hidden;
& > h2 {
height: 88px;
line-height: 88px;
padding: 0 24px;
color: #333333;
font-size: 30px;
}
.template-item {
display: flex;
align-items: center;
height: 104px;
padding: 0 24px;
border-bottom: 1px solid #D8DDE6;
&:active {
background-color: #eee;
}
&:last-child {
border: none;
}
image {
width: 36px;
height: 42px;
}
i {
font-size: 30px;
color: #E1E2E3;
}
h2 {
flex: 1;
padding: 0 18px;
color: #333333;
font-size: 28px;
font-weight: normal;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
}
}
.form-list {
display: flex;
flex-wrap: wrap;
padding: 0 32px 0;
div {
box-sizing: border-box;
}
.form-list__item {
width: calc(50% - 13px);
height: 216px;
margin: 32px 24px 0 0;
padding: 40px 20px 52px;
background-color: #FFFFFF;
border-radius: 8px;
background-size: 100% 100%;
&:active {
background-color: #eee;
}
&:nth-of-type(2n) {
margin-right: 0;
}
div {
width: 148px;
height: 48px;
line-height: 48px;
text-align: center;
color: #fff;
font-size: 28px;
background: #6BA1F9;
border-radius: 24px;
}
h2 {
margin-bottom: 32px;
padding-left: 10px;
color: #333333;
font-weight: 700;
font-size: 32px;
}
}
}
</style>

View File

@@ -0,0 +1,497 @@
<template>
<div class="form">
<ai-top-fixed>
<u-search placeholder="请输入标题" :show-action="false" search-icon-color="#ccc" v-model="search.title"
@search="isMore = false, search.current = 1, getList()"/>
</ai-top-fixed>
<div class="form-list">
<div class="form-item" v-for="(item, index) in list" :key="index"
@click="info = item, id = item.id, isShow = true">
<div class="form-item__top">
<div class="form-item__left">
<h2>{{ item.title }}</h2>
<div class="form-item__left--info">
<span>{{ item.createUserName }}</span>
<span>{{ item.createUnitName }}</span>
<span>{{ item.createTime.substr(0, item.createTime.length - 3) }}</span>
<span>{{ $dict.getLabel('questionnaireType', item.type) }}</span>
</div>
</div>
<div class="form-item__right">
<h2>{{ item.dataCount }}</h2>
<span>答卷数量</span>
</div>
</div>
<div class="form-item__bottom form-item__bottom--active">
<i :style="{background: $dict.getColor('questionnaireStatus', item.status)}"></i>
<span>{{ $dict.getLabel('questionnaireStatus', item.status) }}</span>
</div>
</div>
<ai-empty v-if="!list.length && isMore"></ai-empty>
</div>
<u-popup v-model="isShow" :closeable="false" mode="bottom" :z-index="11">
<div class="popup">
<h2>{{ info.title }}</h2>
<div class="operate-list">
<div class="operate-item" @click="toEdit">
<div>
<image :src="`${$cdn}askform/bj.png`"/>
</div>
<h3>编辑</h3>
</div>
<div class="operate-item" @click="linkTo('/pages/askForm/askForm?preview=1&id=' + id)">
<div>
<image :src="`${$cdn}askform/yl.png`"/>
</div>
<h3>预览</h3>
</div>
<div class="operate-item" @click="publish" v-if="info.status !== '1'">
<div>
<image :src="`${$cdn}askform/fb.png`"/>
</div>
<h3>发布</h3>
</div>
<div class="operate-item" @click="toStop" v-if="info.status === '1'">
<div>
<image :src="`${$cdn}askform/stop.png`"/>
</div>
<h3>停止</h3>
</div>
<div class="operate-item" @click="showShare">
<div>
<image :src="`${$cdn}askform/fx.png`"/>
</div>
<h3>分享</h3>
</div>
<div class="operate-item" @click="share(id)">
<div>
<image :src="`${$cdn}askform/mb.png`"/>
</div>
<h3>共享为模板</h3>
</div>
<div class="operate-item" @click="remove(id)">
<div>
<image :src="`${$cdn}askform/sc.png`"/>
</div>
<h3>删除</h3>
</div>
</div>
<div class="popup-btn" @click="isShow = false">关闭</div>
</div>
</u-popup>
</div>
</template>
<script>
import AiTopFixed from '@/components/AiTopFixed'
import AiEmpty from '@/components/AiEmpty/AiEmpty'
import {mapActions} from 'vuex'
export default {
name: 'formList',
label: '表单列表',
data() {
return {
search: {
current: 1,
templateType: 0,
title: ''
},
id: '',
info: {},
isMore: false,
list: [],
isShow: false
}
},
components: {
AiEmpty,
AiTopFixed
},
mounted() {
this.injectJWeixin(['sendChatMessage', 'selectEnterpriseContact'])
this.$dict.load(['questionnaireStatus', 'questionnaireType', 'questionnaireFieldType']).then(() => {
this.getList()
})
},
methods: {
...mapActions(['injectJWeixin', 'wxInvoke']),
linkTo(url) {
this.isShow = false
uni.navigateTo({
url
})
},
toStop() {
this.$http.post(`/app/appquestionnairetemplate/stopRelease?id=${this.info.id}`).then(res => {
if (res.code === 0) {
this.$u.toast('停止成功')
this.search.current = 1
this.isShow = false
this.isMore = false
this.getList()
}
})
},
showShare() {
if (this.info.status !== '1') {
this.isShow = false
return this.$u.toast(`该表单${this.info.status === '0' ? '未发布' : '已截止'},无法分享!`)
}
uni.showActionSheet({
itemList: ['分享', '微信分享', '获取链接'],
success: data => {
this.$http.post(`/app/appquestionnairetemplate/queryQrCode?id=${this.info.id}`).then(res => {
if (res.code == 0) {
if (data.tapIndex === 2) {
this.copy(res.data.linkUrl)
this.isShow = false
}
if (data.tapIndex === 0 || data.tapIndex === 1) {
this.injectJWeixin(['shareAppMessage', 'shareWechatMessage']).then(() => {
if (data.tapIndex === 0) {
this.wxInvoke(['shareAppMessage', {
title: this.info.title,
desc: this.info.tableExplain,
link: res.data.linkUrl,
imgUrl: this.info.headPicture
}, () => {
this.isShow = false
}])
} else {
this.wxInvoke(['shareWechatMessage', {
title: this.info.title,
desc: this.info.tableExplain,
link: res.data.linkUrl,
imgUrl: this.info.headPicture
}, () => {
this.isShow = false
}])
}
})
}
}
})
}
})
},
copy(link) {
let oInput = document.createElement('input')
oInput.value = link
document.body.appendChild(oInput)
oInput.select()
document.execCommand('Copy')
this.$u.toast('已复制')
oInput.remove()
},
publish() {
if (this.info.status === '1') {
return this.$u.toast('该表单已发布')
}
this.linkTo(`/pages/askForm/formSetting?id=${this.info.id}&type=edit`)
this.isShow = false
},
toEdit() {
if (this.info.dataCount !== 0) {
return this.$u.toast('该表单已有数据,无法编辑!')
}
uni.navigateTo({
url: `/pages/askForm/addForm?id=${this.id}`
})
this.isShow = false
},
share(id) {
this.$http.post(`/app/appquestionnairetemplate/share?id=${id}`).then(res => {
if (res.code === 0) {
this.$confirm('调查表单共享成功,其他成员可在新建项目时直接使用!', '', {
showCancel: false
})
this.isShow = false
}
})
},
remove(id) {
if (this.info.dataCount !== 0) {
return this.$u.toast('该表单已有数据,无法删除!')
}
this.$confirm('确定删除该数据?').then(() => {
this.$http.post(`/app/appquestionnairetemplate/delete?id=${id}`).then(res => {
if (res.code == 0) {
this.$u.toast('删除成功')
this.isShow = false
this.search.current = 1
this.isMore = false
this.getList()
}
})
}).catch(() => {
})
},
reload() {
this.isMore = false
this.search.current = 1
this.getList()
},
getList() {
if (this.isMore) return
this.$http.post(`/app/appquestionnairetemplate/list`, null, {
params: {
...this.search,
size: 10
}
}).then(res => {
if (res.code == 0) {
if (this.search.current > 1) {
this.list = [...this.list, ...res.data.records]
} else {
this.list = res.data.records
}
uni.hideLoading()
if (res.data.records.length < 10) {
this.isMore = true
return false
}
this.search.current = this.search.current + 1
} else {
uni.hideLoading()
}
}).catch(() => {
uni.hideLoading()
})
}
}
}
</script>
<style lang="scss" scoped>
.form {
::v-deep .u-search {
margin-bottom: 0 !important;
}
.popup {
background: #F7F7F7;
& > h2 {
height: 72px;
line-height: 72px;
padding: 0 20px;
color: #999999;
font-size: 22px;
text-align: center;
border-bottom: 2px solid #D7D8DA;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.popup-btn {
height: 96px;
line-height: 96px;
text-align: center;
color: #333333;
font-size: 30px;
background: #fff;
&:active {
background: #eee;
}
}
.operate-list {
display: flex;
flex-wrap: wrap;
text-align: center;
padding-bottom: 26px;
.operate-item {
width: 25%;
font-size: 0;
margin-top: 28px;
&:active {
opacity: 0.7;
}
h3 {
margin-top: 20px;
color: #666666;
font-size: 26px;
font-weight: normal;
}
}
image {
width: 100px;
height: 100px;
border-radius: 16px;
background: #fff;
}
}
}
div {
box-sizing: border-box;
}
.form-item {
margin: 24px 25px 0;
padding: 32px;
background: #FFFFFF;
border-radius: 16px;
.form-item__bottom {
display: flex;
align-items: center;
margin-top: 28px;
color: #999999;
i {
width: 12px;
height: 12px;
margin-right: 6px;
border-radius: 50%;
background: #999999;
}
span {
font-size: 26px;
}
&.form-item__bottom--active i {
background: #3CB300;
}
}
.form-item__top {
display: flex;
justify-content: space-between;
align-items: center;
.form-item__right {
text-align: center;
h2 {
line-height: 40px;
margin-bottom: 16px;
font-size: 32px;
font-weight: 600;
color: #1EA0FA;
}
span {
color: #999999;
font-size: 22px;
}
}
.form-item__left {
flex: 1;
max-width: 80%;
position: relative;
&::after {
position: absolute;
right: -15px;
top: 50%;
width: 2px;
height: 96px;
background: #F5F5F5;
content: '';
transform: translateY(-50%);
}
h2 {
line-height: 44px;
margin-bottom: 16px;
color: #333;
font-size: 32px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.form-item__left--info {
display: flex;
align-items: center;
flex-wrap: wrap;
color: #999;
font-size: 20px;
span {
position: relative;
margin-right: 24px;
&::after {
position: absolute;
right: -12px;
top: 50%;
width: 2px;
height: 20px;
background: #D1D2D5;
content: '';
transform: translateY(-50%);
}
&:last-child {
margin-right: 0;
&::after {
display: none;
}
}
}
}
}
}
}
.type-0 {
background: #2266FF !important;
}
.type-1 {
background: rgba(34, 170, 153, 1) !important;
}
.type-2 {
background: rgba(248, 180, 37, 1) !important;
}
.type-3 {
background: rgba(102, 119, 187, 1) !important;
}
.type-4 {
background: rgba(236, 68, 97, 1) !important;
}
}
</style>

163
src/pages/askForm/config.js Normal file
View File

@@ -0,0 +1,163 @@
export const components = [
{
type: 'radio',
label: '单选',
fixedLabel: '单选',
value: '',
points: '',
icon: 'iconradio',
isShowPoints: false,
required: true,
hasAnswer: false,
answer: '',
pointType: '0',
pointDict: [
{
dictName: '此题有唯一答案和分值',
dictValue: '0'
},
{
dictName: '每个选项都有对应分值',
dictValue: '1'
}
],
options: [
{
label: '选项1',
value: '',
point: '',
img: []
},
{
label: '选项2',
value: '',
point: '',
img: []
}
],
title: ''
},
{
type: 'checkbox',
label: '多选',
fixedLabel: '多选',
points: '',
icon: 'iconcheck_box',
isShowPoints: false,
required: true,
hasAnswer: false,
answer: [],
value: [],
pointType: '0',
pointDict: [
{
dictName: '此题有唯一答案和分值',
dictValue: '0'
},
{
dictName: '每个选项都有对应分值',
dictValue: '1'
},
{
dictName: '答对几项得几分,答错不得分',
dictValue: '2'
}
],
options: [
{
label: '选项1',
value: '',
point: '',
img: [],
checked: false
},
{
label: '选项2',
point: '',
value: '',
img: [],
checked: false
}
],
title: ''
},
{
type: 'select',
label: '单下拉框',
fixedLabel: '单下拉框',
value: '',
points: '',
icon: 'iconSelect',
isShowPoints: false,
required: true,
hasAnswer: false,
answer: '',
pointType: '0',
placeholder: '请选择',
pointDict: [
{
dictName: '此题有唯一答案和分值',
dictValue: '0'
},
{
dictName: '每个选项都有对应分值',
dictValue: '1'
}
],
options: [
{
label: '选项1',
value: '',
point: '',
img: []
},
{
label: '选项2',
value: '',
point: '',
img: []
}
],
title: ''
},
{
type: 'input',
label: '单行填空',
fixedLabel: '单行填空',
value: '',
pointType: '0',
icon: 'icontext_box',
isShowPoints: false,
points: '',
required: true,
hasAnswer: false,
placeholder: '请输入...',
answer: ''
},
{
type: 'textarea',
label: '多行填空',
fixedLabel: '多行填空',
pointType: '0',
icon: 'icontext_area',
points: '',
isShowPoints: false,
required: true,
hasAnswer: false,
answer: '',
placeholder: '请输入...',
value: ''
},
{
type: 'upload',
label: '上传图片',
fixedLabel: '上传图片',
value: '',
icon: 'iconpic',
isShowPoints: false,
points: '',
required: true,
hasAnswer: false,
answer: ''
}
];

View File

@@ -0,0 +1,535 @@
<template>
<div class="form-config">
<div class="config-group">
<div class="config-item">
<u-input class="form-maintitle" :maxlength="200" v-model="config.label" :placeholder="`请输入${config.fixedLabel}标题 ${config.required ? '(必填)' : ''}`" placeholder-style="color: #999999; font-weight: 600" />
</div>
<div class="config-item__select--wrapper" v-if="['radio', 'select', 'checkbox'].includes(config.type)">
<div class="config-item__select" v-for="(item, index) in config.options" :key="index">
<image class="config-icon" :src="`${$cdn}askform/del.png`" @click="removeOptions(index)" />
<div class="config-item__upload" v-if="config.type !== 'select'" @click="upload(index)">
<u-icon color="#8c9dc3" name="plus" v-if="!item.img.length"></u-icon>
<image v-else :src="item.img[0].url" />
</div>
<div class="textarea">
<textarea type="textarea" placeholder-style="color: #CDCDCF" :auto-height="true" v-model="item.label" :maxlength="100" placeholder="请输入选项" />
</div>
</div>
<div class="config-item__select config-item__select--add" @click="addOptions">
<image class="config-icon" :src="`${$cdn}askform/zj.png`" />
<span>添加选项</span>
</div>
</div>
</div>
<div class="config-group">
<div class="config-item" v-if="!['radio', 'upload', 'checkbox', 'select'].includes(config.type)">
<div class="config-item__left">
<span>说明文字</span>
</div>
<div class="config-item__right">
<u-input v-model="config.placeholder" placeholder="请输入说明文字" input-align="right" />
</div>
</div>
<div class="config-item">
<div class="config-item__left">
<span>是否必填</span>
</div>
<div class="config-item__right">
<u-switch v-model="config.required" active-value="1" inactive-value="0" :size="40" active-color="#1088F9"></u-switch>
</div>
</div>
<div class="config-item" v-if="!['upload'].includes(config.type)">
<div class="config-item__left">
<span>答案与分值</span>
</div>
<div class="config-item__right">
<u-switch v-model="config.isShowPoints" :size="40" active-color="#1088F9"></u-switch>
</div>
</div>
<div class="config-item" v-if="['input', 'textarea'].includes(config.type) && config.isShowPoints">
<div class="config-item__left">
<span>正确答案</span>
</div>
<div class="config-item__right">
<u-input v-model="config.answer" placeholder="请输入正确答案" input-align="right" />
</div>
</div>
<div class="config-item" v-if="['radio', 'select'].includes(config.type) && config.isShowPoints && config.pointType === '0'">
<div class="config-item__left">
<span>正确答案</span>
</div>
<div class="config-item__right config-item__text" @click="isShowAnswer = true">
<span>{{ config.answer ? config.answer : '请选择正确答案' }}</span>
<u-icon name="arrow-down-fill" color="#c0c4cc" size="24"></u-icon>
</div>
</div>
<div class="config-item config-item__checkbox" v-if="config.isShowPoints && ['radio', 'select', 'checkbox'].includes(config.type)">
<div class="config-item__left">
<span>计分方式</span>
</div>
<div class="config-item__right" @click="isShowType = true">
<span>{{ pointTypeName ? pointTypeName : '请选择' }}</span>
<u-icon name="arrow-right" color="#E1E2E3" />
</div>
</div>
<div class="config-item config-item__answer config-item__checkbox" v-if="['checkbox'].includes(config.type) && config.isShowPoints && config.pointType === '0'">
<div class="config-item__left">
<span>正确答案</span>
</div>
<div class="config-item__right">
<u-checkbox-group wrap @change="onCheckboxChange">
<u-checkbox v-model="field.checked" :name="field.label" v-if="field.label" v-for="(field, i) in config.options" :key="i">
<span>{{ field.label }}</span>
</u-checkbox>
</u-checkbox-group>
</div>
</div>
<div class="config-item" v-if="config.isShowPoints && config.pointType === '0'">
<div class="config-item__left">
<span>本题分值</span>
</div>
<div class="config-item__right">
<u-input v-model="config.points" type="number" placeholder="请输入本题分值" input-align="right" />
</div>
</div>
<div v-if="config.isShowPoints && config.pointType === '1'">
<div class="config-item" v-for="(item, index) in config.options" :key="index">
<div class="config-item__left">
<span>{{ item.label }}</span>
</div>
<div class="config-item__right">
<u-input v-model="item.point" placeholder="请输入分值" input-align="right" />
</div>
</div>
</div>
<div class="config-item config-item__point" v-if="config.isShowPoints && config.pointType === '2'">
<u-checkbox-group wrap @change="onCheckboxChange">
<u-checkbox v-model="field.checked" :name="field.label" v-if="field.label" v-for="(field, i) in config.options" :key="i">
<span>{{ field.label }}</span>
<u-input v-model="field.point" type="number" placeholder="请输入分值" input-align="right" />
</u-checkbox>
</u-checkbox-group>
</div>
<div class="config-item" v-if="config.isShowPoints && config.pointType === '2'">
<div class="config-item__left" style="padding-left: 20px">
<span>全部答对</span>
</div>
<div class="config-item__right">
<u-input v-model="config.points" type="number" placeholder="请输入全部答对分值" input-align="right" />
</div>
</div>
</div>
<u-select :list="config.options" :default-value="defaultAnswer" value-name="value" label-name="label" v-model="isShowAnswer" @confirm="answerChange"></u-select>
<u-select :list="config.pointDict" :default-value="defaultType" value-name="dictValue" label-name="dictName" v-model="isShowType" @confirm="pointTypeChange"></u-select>
<div class="add-form__footer">
<div @click="back">
<span>取消</span>
</div>
<div @click="confirm">确定</div>
</div>
</div>
</template>
<script>
import { components } from './config'
export default {
data () {
return {
index: '',
isShowType: false,
isShowAnswer: false,
config: {
}
}
},
onLoad (query) {
if (query.type) {
this.config = JSON.parse(JSON.stringify(components.filter(v => v.type === query.type)[0]))
} else {
this.config = JSON.parse(query.config)
this.index = query.index
}
},
computed: {
pointTypeName () {
if (!this.config.pointDict) return ''
return this.config.pointDict.filter(v => v.dictValue === this.config.pointType)[0].dictName
},
defaultType () {
if (!this.config.pointType) return [0]
return [Number(this.config.pointType)]
},
defaultAnswer () {
if (!this.config.answer) return [0]
let index = 0
if (this.config.answer) {
this.config.options.forEach((v, i) => {
if (v.label === this.config.answer) {
index = i
}
})
}
return [index]
}
},
methods: {
answerChange (e) {
this.config.answer = e[0].label
},
pointTypeChange (e) {
console.log(e)
this.config.pointType = e[0].value
},
onCheckboxChange (e) {
this.config.answer = e
},
upload (index) {
let params = {
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
let count = this.fileList?.length + (res.tempFiles?.length || res.tempFile ? 1 : 0)
if (count > 1) {
return this.$u.toast(`不能超过1个`)
}
if (res.tempFiles) {
res.tempFiles.map((item) => {
this.uploadFile(item, index)
})
}
}
}
uni.chooseImage(params)
},
uploadFile (img, index) {
uni.showLoading({title: '上传中'})
let formData = new FormData()
formData.append('file', img)
this.$http.post('/admin/file/add2', formData).then((res) => {
uni.hideLoading()
if (res?.data) {
this.$u.toast('上传成功!')
this.$set(this.config.options[index], 'img', [res.data])
}
}).catch(res => {
this.$u.toast(res)
uni.hideLoading()
})
},
removeOptions (index) {
const len = this.config.options.length
const label = this.config.options[index].label
if (len === 2) {
return this.$u.toast('选项不能少于2个')
}
if (this.config.type === 'checkbox') {
const answerIndex = this.config.answer.indexOf(label)
if (answerIndex > -1) {
this.config.answer.splice(answerIndex, 1)
}
} else {
if (label === this.config.answer) {
this.config.answer = ''
}
}
this.config.options.splice(index, 1)
},
back () {
uni.navigateBack({
delta: 1
})
},
confirm () {
uni.$emit('filedConfig', {
config: this.config,
index: this.index === '' ? '-1' : this.index
})
uni.navigateBack({
delta: 1
})
},
addOptions () {
const len = this.config.options.length
let label = `选项${len + 1}`
const index= this.config.options.findIndex(v => label === v.label)
if (index > -1) {
label = `新选项${len + 1}`
}
this.config.options.push({
label: label,
value: '',
point: '',
img: '',
checked: false
})
},
}
}
</script>
<style lang="scss" scoped>
.form-config {
box-sizing: border-box;
padding-bottom: 130px;
.form-maintitle {
::v-deep .uni-input-input {
font-size: 36px;
font-weight: 600;
}
}
.config-item__select--wrapper {
.config-item__select {
display: flex;
align-items: center;
::v-deep .u-input__input {
height: 100%;
}
.textarea {
flex: 1;
display: flex;
align-items: center;
min-height: 104px;
padding: 16px 0;
font-size: 28px;
border-bottom: 1px solid #dfe8f8;
}
textarea {
width: 100%;
font-size: 28px;
}
.config-icon {
width: 36px;
height: 36px;
margin-right: 12px;
}
}
.config-item__select--add {
height: 120px;
.config-icon {
margin-right: 18px;
}
span {
color: #1D74F4;
font-size: 30px;
}
}
}
.config-group {
margin-bottom: 32px;
padding: 0 32px;
background: #fff;
.config-item__upload {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
margin-right: 20px;
border: 1px solid rgb(208, 212, 220);
background-color: #fbfdff;
image {
width: 100%;
height: 100%;
}
}
.config-item {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 100px;
padding: 16px 0;
border-bottom: 1px solid #dfe8f8;
&:last-child {
border: none;
}
::v-deep .u-radio__label, ::v-deep .u-checkbox__label {
margin-right: 0;
font-size: 28px;
span {
max-width: 400rpx;
line-height: 1.2;
}
}
.config-item__left {
max-width: 400px;
}
.config-item__right {
flex: 1;
text-align: right;
padding-left: 30px;
&.config-item__text {
display: flex;
align-items: center;
justify-content: flex-end;
span {
max-width: 400px;
margin-right: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: keep-all;
}
}
.text {
}
}
&.config-item__answer {
display: block;
padding: 20px 0;
.config-item__left {
margin-bottom: 32rpx;
span {
word-break: break-all;
color: #333;
font-size: 30px;
}
}
.config-item__right {
width: 100%;
padding-left: 0;
text-align: left;
}
}
}
::v-deep .u-checkbox {
align-items: baseline;
}
.config-item__checkbox {
height: auto;
padding: 14px 0;
::v-deep .u-checkbox, ::v-deep .u-radio {
// justify-content: flex-end;
}
}
.config-item__point {
height: auto;
padding: 0;
::v-deep .u-checkbox {
justify-content: inherit;
min-height: 100px;
padding: 14px 0;
border-bottom: 1px solid #eee;
&:last-child {
border: none;
}
.u-checkbox__label {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
margin-right: 0;
.u-input {
flex: 1;
max-width: 400px;
}
}
}
}
}
* {
box-sizing: border-box;
}
.add-form__footer {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
width: 100%;
height: 112px;
text-align: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-align: center;
background: #fff;
&:first-child:active {
background: #eee;
}
&:last-child {
color: #fff;
font-size: 36px;
background: #3192F4;
&:active {
opacity: 0.8;
}
}
span {
flex: 1;
height: 100%;
line-height: 112px;
color: #333333;
font-size: 32px;
&:active {
background: #eee;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,405 @@
<template>
<section class="formDetail">
<ai-result v-if="result.tips" v-bind="result">
<template v-if="isExam" #extra>
<div flex class="scorePane">
<div>成绩</div>
<div class="fill"><em v-html="score"/> </div>
</div>
</template>
</ai-result>
<template v-else-if="form.id">
<image v-if="form.headPicture" class="headPicture" :src="form.headPicture"/>
<b class="title">{{ form.title || "标题" }}</b>
<div class="tableExplain">{{ form.tableExplain }}</div>
<u-form class="content" label-position="top">
<u-form-item class="item" v-for="(op,i) in fields" :key="i" :label="(i+1)+'.'+op.fieldName"
:required="op.fieldInfo.required==1">
<template v-if="op.fieldType=='input'">
<input v-model="op.fieldValue" :placeholder="op.fieldInfo.placeholder" :disabled="isResult"/>
</template>
<template v-else-if="op.fieldType=='textarea'">
<textarea v-model="op.fieldValue" :disabled="isResult" :placeholder="op.fieldInfo.placeholder"/>
</template>
<template v-else-if="op.fieldType=='upload'">
<ai-uploader @list="v=>op.fieldValue=v.map(e=>e.url)" :def="op.fieldValue" :disabled="isResult"
preview/>
</template>
<u-row v-else-if="op.fieldType=='radio'">
<radio-group @change="({detail})=>op.fieldValue=detail.value">
<div class="option" flex v-for="option in op.fieldInfo.options" :key="option.label">
<radio :value="option.label" :disabled="isResult" :checked="op.fieldValue==option.label"/>
<ai-image v-if="option.img" :src="option.img" preview/>
<div class="label fill">{{ option.label }}</div>
</div>
</radio-group>
</u-row>
<u-row v-else-if="op.fieldType=='checkbox'">
<checkbox-group @change="({detail})=>op.fieldValue=detail.value">
<div class="option" flex v-for="option in op.fieldInfo.options" :key="option.label">
<checkbox :value="option.label" :disabled="isResult"
:checked="option.checked"/>
<ai-image v-if="option.img" :src="option.img" preview/>
<div class="label fill">{{ option.label }}</div>
</div>
</checkbox-group>
</u-row>
<template v-else-if="op.fieldType=='select'">
<ai-select @data="v=>op.fieldValue=v.map(e=>e.value)" :list="op.fieldInfo.options" :disabled="isResult">
<div class="option" flex>
<div class="label fill" v-if="op.fieldValue">{{ op.fieldValue.toString() }}</div>
<i class="fill" v-else>请选择</i>
<u-icon name="arrow-right" color="#ddd"/>
</div>
</ai-select>
</template>
</u-form-item>
</u-form>
<div class="bottom" v-if="!(isPreview||isResult)">
<div class="bottomBtn" @tap="handleSubmit">提交</div>
</div>
</template>
<ai-loading v-else tips="调查问卷加载中..."/>
</section>
</template>
<script>
import UForm from "../../uview/components/u-form/u-form";
import UFormItem from "../../uview/components/u-form-item/u-form-item";
import {mapActions, mapState} from "vuex";
import UInput from "../../uview/components/u-input/u-input";
import AiTextarea from "../../components/AiTextarea";
import AiUploader from "../../components/AiUploader";
import AiSelect from "../../components/AiSelect";
import URadio from "../../uview/components/u-radio/u-radio";
import AiLoading from "../../components/AiLoading";
import AiResult from "../../components/AiResult";
import AiImage from "../../components/AiImage";
import AiBack from "../../components/AiBack";
export default {
name: "formDetail",
components: {
AiBack,
AiImage,
AiResult,
AiLoading,
URadio,
AiSelect,
AiUploader,
AiTextarea,
UInput,
UFormItem,
UForm
},
computed: {
...mapState(['openUser', 'token']),
isExam() {
return this.form?.type == 1
},
isPreview() {
return !!this.$route.query?.preview
},
isResult() {
return !!this.$route.query?.result
}
},
data() {
return {
form: {},
fields: [],
checkUser: false,
result: {},
score: 0
}
},
watch: {
form: {
deep: true,
handler(v) {
this.fields = v?.fields?.map(e => {
let fieldInfo = JSON.parse(e.fieldInfo)
fieldInfo?.options?.map(op => {
op.img = op?.img?.[0]?.url
op.checked = !!e.fieldValue?.split(",")?.includes(op.label)
})
if (e.fieldType == 'select') {
fieldInfo.options = fieldInfo.options.map(e => ({...e, value: e.label, label: e.label}))
}
return {...e, fieldInfo}
}) || []
}
}
},
methods: {
...mapActions(['getUserInfo']),
getForm() {
let {id} = this.$route.query
this.$http.post("/app/appquestionnairetemplate/queryDetailById", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.form = res.data
}
})
},
validateForm() {
return !this.fields.some(e => {
if (!!e?.fieldInfo?.required && !e.fieldValue?.toString()) {
this.$u.toast(e.fieldName + "不能为空!")
return true
}
})
},
handleSubmit() {
if (this.validateForm()) {
this.handleScore()
let {avatar: avatarUrl, openId, name: nickName, type: userType, unionId, corpName} = this.openUser
this.$http.post("/app/appquestionnairetemplate/commit", {
fields: this.fields.map(e => ({
...e,
fieldInfo: JSON.stringify(e.fieldInfo),
fieldValue: e.fieldValue?.toString()
})),
avatarUrl, openId, nickName, userType, unionId, corpName,
totalScore: this.score,
questionnaireTemplateId: this.$route.query.id
}).then(res => {
if (res?.code == 0) {
this.result = {
tips: "提交成功!感谢参与",
}
}
})
}
},
handleScore() {
this.score = 0
this.isExam && this.fields.map(field => {
let item = field?.fieldInfo || {}
let current = 0
const calcScore = point => (current += (Number(point) || 0))
if (item?.pointType == 0) {//此题有唯一答案和分值
field.fieldValue?.toString() == item.answer?.toString() && calcScore(item?.points)
} else if (item?.pointType == 1) {//每个选项都有对应分值
item?.options?.map(op => {
if (typeof field.fieldValue == "object") {
if (field.fieldValue?.includes(op.label)) {
calcScore(op.point)
}
} else {
op.label == field.fieldValue && calcScore(op.point)
}
})
} else if (item?.pointType == 2) {//答对几项得几分,答错不得分
item?.options?.some(op => {
if (typeof field.fieldValue == "object") {
if (field.fieldValue?.includes(op.label)) {
if (item.answer?.includes(op.label)) calcScore(op.point)
else return current = 0
}
} else {
op.label == field.fieldValue && calcScore(op.point)
}
})
}
this.score += current
//打印每题打分
if (!!field.fieldValue) {
const typeResult = (reply, answer) => {
console.log("题目:%s,回答:%s,得分:%s,总分:%s \n 答案:%s", field.fieldName,
reply, current, this.score, answer)
}
typeResult(field.fieldValue?.toString(), item.answer?.toString())
}
})
},
checkForm() {
if (this.isPreview) {
this.checkUser = true
return Promise.resolve()
}
let {query: {id}, hash} = this.$route,
{openId} = this.openUser
if (hash != "#form") {
this.result = {
tips: "非法的调查问卷链接",
status: "error"
}
} else if (openId) {
return new Promise(resolve => {
this.$http.post("/app/appquestionnairetemplate/commitCheck", null, {
params: {id, openId}
}).then(res => {
if (res?.code == 0) {
this.checkUser = true
if (this.isResult && res?.data) {
this.form = res?.data
} else resolve()
} else this.result = {
tips: "调查问卷加载失败",
status: "error",
btn: "重新加载",
btnTap() {
location.reload()
}
}
}).catch(err => {
this.result = {
tips: err || "调查问卷加载失败",
}
})
})
} else {
this.getUserInfo().then(() => {
if (!!this.openUser?.openId) {
this.checkForm()?.then(() => this.checkUser && this.getForm())
} else {
this.result = {
tips: "您的信息获取失败",
status: "error",
btn: "重新加载",
btnTap() {
location.reload()
}
}
}
})
}
},
},
created() {
this.checkForm()?.then(() => this.checkUser && this.getForm())
},
mounted() {
document.title = this.form.title || "调查问卷"
}
}
</script>
<style lang="scss" scoped>
.formDetail {
display: flex;
flex-direction: column;
background: #fff;
.headPicture {
width: 100%;
height: 320px;
.img {
width: 100%;
}
}
.title {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
padding: 32px;
box-sizing: border-box;
font-size: 34px;
font-weight: bold;
color: #333;
line-height: 48px;
}
.bottom {
position: fixed;
bottom: 0;
width: 100%;
padding: 32px;
box-sizing: border-box;
background: #fff;
z-index: 99;
.bottomBtn {
width: 100%;
line-height: 96px;
background: #287DE1;
color: #fff;
text-align: center;
height: 96px;
font-size: 32px;
font-weight: bold;
border-radius: 8px;
}
}
.tableExplain {
font-size: 28px;
font-weight: 400;
color: #666;
padding: 32px 24px;
}
::v-deep .u-form-item {
.u-form-item--left {
font-size: 30px;
font-weight: bold;
color: #333;
}
.u-form-item--right__content__slot > * {
width: 100%;
}
.display {
justify-content: space-between;
min-height: 58px;
}
.uni-radio-input, .uni-checkbox-input {
height: 32px;
width: 32px;
}
.label {
flex-shrink: 0;
* + & {
margin-left: 16px;
}
}
.option {
width: 100%;
margin-right: 16px;
margin-bottom: 16px;
}
}
.content {
padding: 64px 32px 200px;
background: #fff;
}
::v-deep .scorePane {
width: calc(100% - 40px);
padding: 0 32px;
height: 124px;
background: #E9F2FF;
border-radius: 16px;
font-size: 30px;
font-weight: 500;
color: #333333;
margin-top: 48px;
box-sizing: border-box;
.fill {
text-align: center;
}
em {
font-size: 48px;
font-weight: bold;
color: #2C72FE;
font-style: normal;
margin-right: 8px;
}
}
}
</style>

View File

@@ -0,0 +1,164 @@
<template>
<section class="formList">
<ai-top-fixed>
<u-search placeholder="请输入标题" :show-action="false" search-icon-color="#ccc"
v-model="search.title" @search="page.current=1,getList()"/>
</ai-top-fixed>
<div class="mainPane">
<div class="formBox column" flex v-for="op in list" :key="op.id">
<div flex>
<div class="fill column" flex>
<b class="title">{{ op.title }}</b>
<div class="info wrap" flex>
<span v-html="op.createUserName"/>
<span v-html="op.createUnitName"/>
<span v-html="op.createTime"/>
<span v-html="$dict.getLabel('questionnaireType',op.type)"/>
</div>
</div>
<div class="split"/>
<div flex class="column submitCount">
<b>{{ op.dataCount }}</b>
<div>答卷数量</div>
</div>
</div>
<div flex class="bottom">
<div class="dot" :style="{background:$dict.getColor('questionnaireStatus',op.status)}"/>
<div>{{ $dict.getLabel("questionnaireStatus", op.status) }}</div>
</div>
</div>
</div>
</section>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "formList",
components: {AiTopFixed},
data() {
return {
page: {current: 1, size: 10, total: 0},
search: {title: ""},
list: []
}
},
methods: {
getList() {
this.$http.post("/app/appquestionnairetemplate/list", null, {
params: {...this.page, ...this.search}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
}
},
created() {
this.$dict.load("questionnaireStatus", 'questionnaireType')
this.getList()
}
}
</script>
<style lang="scss" scoped>
.formList {
min-height: 100%;
background: #F3F6F9;
::v-deep .mainPane {
padding: 24px 24px 126px;
.formBox {
width: 100%;
min-height: 220px;
background: #FFFFFF;
border-radius: 16px;
padding: 24px;
box-sizing: border-box;
text-align: center;
color: #999;
font-size: 20px;
margin-bottom: 24px;
& > div {
width: 100%;
}
b {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 32px;
color: #333;
margin-bottom: 16px;
}
.title {
text-align: start;
}
.info {
width: 100%;
span {
white-space: nowrap;
padding: 0 16px;
box-sizing: border-box;
border-left: 1px solid #D1D2D5;
margin-bottom: 4px;
&:first-of-type {
border-left: none;
padding-left: 0;
}
}
}
.bottom {
margin-top: 30px;
font-size: 26px;
.dot {
width: 11px;
height: 11px;
background: #3CB300;
border-radius: 50%;
margin-right: 8px;
}
}
.submitCount {
width: 118px;
b {
color: #1EA0FA;
}
& > div {
font-size: 22px;
white-space: nowrap;
}
}
.split {
width: 2px;
background: #f5f5f5;
margin: 0 24px;
}
}
}
}
</style>

View File

@@ -0,0 +1,314 @@
<template>
<div class="form-setting">
<h2>表单设置</h2>
<div class="form-setting__list">
<div class="setting-item">
<div class="setting-item__left">
<span>截止时间</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '表单截止后,用户打开表单会提示此表单已结束' , isShowModal = true" />
</div>
<div class="setting-item__right">
<u-radio-group v-model="periodValidityType" active-color="#1088F9">
<u-radio name="0">永久有效</u-radio>
<u-radio name="1">自定义时间</u-radio>
</u-radio-group>
<u-icon name="arrow-right" color="#E1E2E3" />
</div>
</div>
<div class="setting-item" v-if="periodValidityType === '1'" @click="isShowTime = true">
<div class="setting-item__left">
<span>截至时间</span>
</div>
<div class="setting-item__right">
<span>{{ periodValidityEndTime }}</span>
<u-icon name="arrow-right" color="#E1E2E3" />
</div>
</div>
<div class="setting-item">
<div class="setting-item__left">
<span>匹配客户方式</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '将参与活动的微信客户和企业微信客户匹配' , isShowModal = true" />
</div>
<div class="setting-item__right">
<span>客户微信ID匹配</span>
</div>
</div>
<div class="setting-item">
<div class="setting-item__left">
<span>提交次数</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '此功能发布后不可修改' , isShowModal = true" />
</div>
<div class="setting-item__right">
<u-radio-group v-model="commitType" active-color="#1088F9">
<u-radio name="0">不限次数</u-radio>
<u-radio name="1">限提交一次</u-radio>
</u-radio-group>
</div>
</div>
<div class="setting-item">
<div class="setting-item__left">
<span>行为通知</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '当客户点击或者发布表单时,发送表单的员工将会受到消息提醒' , isShowModal = true" />
</div>
<div class="setting-item__right">
<u-switch v-model="actionNotice" active-value="1" inactive-value="0" :size="40" active-color="#1088F9"></u-switch>
</div>
</div>
<div class="setting-item">
<div class="setting-item__left">
<span>动态通知</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '当客户点击或者发布表单时,会将客户的打开行为记录在客户动态里' , isShowModal = true" />
</div>
<div class="setting-item__right">
<u-switch v-model="dynamicNotice" active-value="1" inactive-value="0" :size="40" active-color="#1088F9"></u-switch>
</div>
</div>
</div>
<div class="add-form__footer">
<div @click="back">
<span>取消</span>
</div>
<div @click="confirm">{{ type === 'edit' ? '发布' : '确定' }}</div>
</div>
<u-modal v-model="isShowModal" :content="tips"></u-modal>
<u-picker mode="time" v-model="isShowTime" :show-time-tag="true" @confirm="onTimeChange" :params="params"></u-picker>
</div>
</template>
<script>
export default {
data () {
return {
params: {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: true
},
tips: '',
isShowModal: false,
actionNotice: true,
dynamicNotice: true,
commitType: '1',
wechatId: '0',
periodValidityEndTime: '',
isShowTime: false,
periodValidityType: '0',
type: '',
id: ''
}
},
onLoad (query) {
if (query.id) {
this.id = query.id
this.getInfo(query.id)
this.type = query.type
} else if (query.formConfig && query.formConfig !== '{}') {
const res = JSON.parse(query.formConfig)
this.periodValidityType = res.periodValidityType
this.commitType = res.commitType
this.actionNotice = res.actionNotice === '1'
this.dynamicNotice = res.dynamicNotice === '1'
if (res.periodValidityType === '1') {
this.periodValidityEndTime = res.periodValidityEndTime
}
}
},
methods: {
onTimeChange (e) {
this.periodValidityEndTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`
},
back () {
uni.navigateBack({
delta: 1
})
},
getInfo (id) {
this.$http.post(`/app/appquestionnairetemplate/queryDetailById?id=${id}`).then(res => {
if (res.code == 0) {
this.periodValidityType = res.data.periodValidityType
this.commitType = res.data.commitType
this.actionNotice = res.data.actionNotice === '1'
this.dynamicNotice = res.data.dynamicNotice === '1'
if (res.data.periodValidityType === '1') {
this.periodValidityEndTime = res.data.periodValidityEndTime
}
}
}).catch(msg => {
this.$u.toast(msg)
})
},
publish () {
this.$http.post(`/app/appquestionnairetemplate/release`, null, {
params: {
commitType: this.commitType,
periodValidityType: this.periodValidityType,
actionNotice: this.actionNotice ? '1' : '0',
dynamicNotice: this.dynamicNotice ? '1' : '0',
shareStatus: '0',
wechatId: '0',
id: this.id,
periodValidityEndTime: this.periodValidityType === '1' ? this.periodValidityEndTime : ''
}
}).then(res => {
if (res.code == 0) {
uni.$emit('reload')
this.$u.toast('发布成功')
this.back()
}
}).catch(e => {
this.$u.toast(e)
})
},
confirm () {
if (this.type === 'edit') {
this.publish()
return false
}
uni.$emit('setting', {
periodValidityType: this.periodValidityType,
commitType: this.commitType,
actionNotice: this.actionNotice ? '1' : '0',
dynamicNotice: this.dynamicNotice ? '1' : '0',
periodValidityEndTime: this.periodValidityEndTime ? this.periodValidityEndTime : ''
})
this.back()
}
}
}
</script>
<style lang="scss" scoped>
.form-setting {
padding: 0 20px;
.add-form__footer {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
width: 100%;
height: 112px;
text-align: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-align: center;
background: #fff;
&:first-child:active {
background: #eee;
}
&:last-child {
color: #fff;
font-size: 36px;
background: #3192F4;
&:active {
opacity: 0.8;
}
}
span {
flex: 1;
height: 100%;
line-height: 112px;
color: #333333;
font-size: 32px;
&:active {
background: #eee;
}
}
}
}
* {
box-sizing: border-box;
}
& > h2 {
height: 80px;
padding-top: 24px;
font-size: 28px;
color: #999999;
}
.form-setting__list {
padding: 0 20px;
background: #fff;
border-radius: 8px;
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 104px;
border-bottom: 1px solid #D8DDE6;
.setting-item__right {
color: #999;
font-size: 28px;
span {
margin-right: 6px;
}
::v-deep .u-radio__label {
color: #999;
font-size: 28px;
}
::v-deep .u-radio {
&:last-child {
.u-radio__label {
margin-right: 6px;
}
}
}
}
&:last-child {
border: none;
}
& > div {
display: flex;
align-items: center;
}
.setting-item__left {
color: #666666;
font-size: 30px;
image {
width: 30px;
height: 30px;
margin-left: 16px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="form">
<div class="form-content">
<add-list ref="addList" v-if="currIndex === 1"></add-list>
<list ref="list" v-if="currIndex === 0"></list>
</div>
<ai-tabbar :active.sync="currIndex" :list="tabBar"/>
<div class="form-footer"></div>
</div>
</template>
<script>
import addList from './components/addList.vue'
import list from './components/list.vue'
import AiTabbar from '../../components/AiTabbar'
export default {
name: 'formIndex',
label: '问卷表单',
data () {
return {
currIndex: 0
}
},
components: {
addList,
AiTabbar,
list
},
computed: {
tabBar () {
const link = icon => `${this.$cdn}askform/${icon}.png`
return [
{text: "表单列表", iconPath: "bdlb1", selectedIconPath: "bdlb2" },
{text: "新建项目", iconPath: "xjxm1", selectedIconPath: "xjxm2" }
].map(e => ({
...e,
iconPath: link(e.iconPath),
selectedIconPath: link(e.selectedIconPath)
}))
}
},
onLoad () {
uni.$on('reload', () => {
if (this.currIndex === 0) {
this.$refs.list.reload()
} else {
this.$refs.addList.getList()
}
})
},
onReachBottom() {
if (this.currIndex === 0) {
this.$refs.list.getList()
}
}
}
</script>
<style lang="scss" scoped>
.form {
padding-bottom: 98px;
}
</style>

View File

@@ -0,0 +1,457 @@
<template>
<div class="add-form">
<div class="header-pic">
<image v-if="form.headPicture" :src="form.headPicture" />
</div>
<div class="form-info">
<div class="form-info__wrapper">
<textarea class="title" :auto-height="true" disabled placeholder="请输入标题 (必填)" v-model="form.title"></textarea>
<u-input class="content" disabled :clearable="false" type="textarea" v-model="form.tableExplain" placeholder="请输入表单描述 (选填)" :height="80" :auto-height="true" :maxlength="255"></u-input>
</div>
</div>
<div class="components-list">
<div class="components-item" v-for="(item, index) in targetList" :key="index">
<div class="components-item__title">
<div class="components-item__title--left">
<em :style="{opacity: item.required ? 1 : 0}">*</em>
<i>{{ index + 1 }}.</i>
<h2>{{ item.label }}</h2>
</div>
</div>
<div class="components-item__filed">
<template v-if="(item.type === 'radio')">
<u-radio-group v-model="item.value" wrap>
<u-radio :name="field.label" v-for="(field, i) in item.options" :key="i">
<image :src="field.img[0].url" v-if="field.img.length"/>
<span>{{ field.label }}</span>
</u-radio>
</u-radio-group>
</template>
<template v-if="(item.type === 'checkbox')">
<u-checkbox-group wrap>
<u-checkbox :name="field.label" v-model="field.checked1" v-for="(field, i) in item.options" :key="i">
<image :src="field.img[0].url" v-if="field.img.length"/>
<span>{{ field.label }}</span>
</u-checkbox>
</u-checkbox-group>
</template>
<template v-if="(item.type === 'select')">
<div class="components-item__select">
<span>{{ item.placeholder }}</span>
<u-icon name="arrow-down" color="#DEDFDF" />
</div>
</template>
<template v-if="(item.type === 'upload')">
<div class="components-item__select components-item__textarea components-item__upload">
<image :src="`${$cdn}askform/upload.png`" />
<span>选择图片2M以内</span>
</div>
</template>
<template v-if="(item.type === 'input')">
<div class="components-item__select">
<span>{{ item.placeholder }}</span>
</div>
</template>
<template v-if="(item.type === 'textarea')">
<div class="components-item__select components-item__textarea">
<span>{{ item.placeholder }}</span>
</div>
</template>
</div>
</div>
</div>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
export default {
data () {
return {
form: {
tableExplain: '详细描述',
title: '问卷调查',
isShowheadPicture: true,
isShowTableExplain: true,
isShowBtn: true,
headPicture: '',
commitType: '1',
periodValidityType: '0',
actionNotice: '1',
dynamicNotice: '1',
periodValidityEndTime: '',
shareStatus: '0',
count: 0,
wechatId: '0',
type: 0,
buttonExplain: '提交',
tips: true
},
templateType: 0,
targetList: [],
isShow: false,
type: 0,
id: '',
touchStart: 0
}
},
onLoad (query) {
this.form = JSON.parse(query.form)
this.targetList = JSON.parse(query.targetList)
},
components: {
AiBack
},
methods: {
getInfo (id) {
uni.showLoading()
this.$http.post(`/app/appquestionnairetemplate/queryDetailById?id=${id}`).then(res => {
if (res.code == 0) {
this.form = {
...res.data,
headPicture: res.data.headPicture
}
this.type = res.data.type
this.targetList = res.data.fields.map(item => {
return JSON.parse(item.fieldInfo)
})
this.pageShow = true
} else {
this.$u.toast(res.msg)
}
uni.hideLoading()
}).catch(() => {
uni.hideLoading()
})
}
}
}
</script>
<style lang="scss" scoped>
.add-form {
min-height: 100vh;
padding-bottom: 60px;
box-sizing: border-box;
background: #F3F6F9;
* {
box-sizing: border-box;
}
::v-deep .u-drawer-bottom {
background-color: transparent;
}
.components-list {
padding: 0 20px;
.components-item {
margin-top: 24px;
padding: 32px;
box-shadow: 0 4px 8px 4px rgba(233, 233, 233, 0.39);
border-radius: 8px;
overflow: hidden;
border: 1px solid #EEEFF0;
background: #fff;
::v-deep .u-radio, ::v-deep .u-checkbox {
position: relative;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.u-checkbox__icon-wrap, .u-radio__icon-wrap {
position: absolute;
top: 4px;
}
.u-radio__label, .u-checkbox__label {
display: flex;
align-items: center;
margin-right: 0;
margin-left: 40px;
line-height: 1.5;
text-align: justify;
image {
width: 100px;
height: 100px;
margin: 0 10px;
}
}
}
span {
flex: 1;
color: #666;
font-size: 26px;
}
.components-item__select {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 80px;
margin-bottom: 8px;
padding: 0 26px;
border: 1px solid #DEDFDF;
&.components-item__textarea {
align-items: flex-start;
height: 160px;
padding-top: 20px;
image {
width: 46px;
height: 34px;
margin-right: 16px;
}
span {
color: #666;
font-size: 26px;
}
}
&.components-item__upload {
justify-content: center;
align-items: center;
}
}
.components-item__title {
display: flex;
justify-content: space-between;
margin-bottom: 32px;
em {
margin-right: 4px;
font-style: normal;
color: rgb(226, 33, 32);;
}
image {
width: 32px;
height: 32px;
box-sizing: content-box;
padding: 20px 0 20px 20px;
}
div {
display: flex;
align-items: baseline;
color: #333333;
font-size: 32px;
i {
font-style: normal;
}
h2 {
font-weight: 600;
font-size: 32px;
}
}
}
}
}
.add-popup {
height: 440px;
border-radius: 20px 20px 0 0;
background: #fff;
.add-popup__title {
display: flex;
position: relative;
align-items: center;
justify-content: center;
height: 96px;
border-bottom: 1px solid #E4E5E6;
h2 {
color: #333333;
font-size: 32px;
font-weight: 600;
}
image {
position: absolute;
right: 32px;
top: 50%;
width: 30px;
height: 20px;
transform: translateY(-50%);
}
}
.add-popup__list {
display: flex;
flex-wrap: wrap;
padding: 0 34px;
span {
width: calc((100% - 64px) / 3);
height: 78px;
line-height: 78px;
margin-top: 32px;
margin-right: 32px;
text-align: center;
color: #333333;
font-size: 28px;
border-radius: 8px;
border: 1px solid #E4E5E6;
&:nth-of-type(3n) {
margin-right: 0;
}
}
}
}
.add-form__footer {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
width: 100%;
height: 112px;
text-align: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-align: center;
background: #fff;
&:last-child {
color: #fff;
font-size: 36px;
background: #3192F4;
&:active {
opacity: 0.8;
}
}
span {
flex: 1;
height: 100%;
line-height: 112px;
color: #333333;
font-size: 32px;
&:active {
background: #eee;
}
}
}
}
.add-form__btn {
display: flex;
align-items: center;
justify-content: center;
width: 214px;
height: 66px;
line-height: 66px;
margin: 64px auto 0;
background: #FFFFFF;
border-radius: 34px;
&:active {
opacity: 0.8;
}
image {
width: 28px;
height: 28px;
}
span {
margin-left: 16px;
color: #4392E6;
font-size: 28px;
}
}
* {
box-sizing: border-box;
}
.form-info {
margin-top: 26px;
padding: 0 20px;
& > h2 {
height: 76px;
line-height: 76px;
color: #999999;
font-weight: normal;
font-size: 28px;
}
.form-info__wrapper {
padding: 0 18px;
background: #fff;
.title {
width: 100%;
padding: 22px 0;
font-size: 36px;
border-bottom: 1px solid #F1F2F3;
}
.content {
padding: 30px 0!important;
font-size: 28px;
::v-deep textarea {
color: #333;
font-size: 28px!important;
}
}
}
}
.header-pic {
position: relative;
font-size: 0;
span {
position: absolute;
bottom: 16px;
right: 16px;
z-index: 1;
width: 148px;
height: 56px;
line-height: 56px;
text-align: center;
color: #fff;
font-size: 26px;
background: rgba(0, 0, 0, 0.16);
border-radius: 28px;
}
image {
width: 100%;
height: 320px;
}
}
}
</style>

View File

@@ -0,0 +1,501 @@
<template>
<div class="addPlay">
<div class="content">
<div class="item">
<div class="label">播发内容</div>
<div class="value" @click="linkTo('/pages/resourcesManage/resourcesManage?isChoose=1')">
<span :class="formData.mediaName == '请选择' ? 'color-999' : ''">{{formData.mediaName}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
<div class="item">
<div class="label">播放设备</div>
<div class="value" @click="selectClick('showEquipment', equipmentList)">
<span :class="formData.serialName == '请选择' ? 'color-999' : ''">{{formData.serialName}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
<div class="item">
<div class="label">播发级别</div>
<div class="value" @click="selectClick('showMessageLevel', messageLevelList)">
<span :class="formData.messageLevelName == '请选择' ? 'color-999' : ''">{{formData.messageLevelName}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
</div>
<div class="radio-content">
<div class="title">播放方式</div>
<div class="item mar-r50" :class="formData.taskType == 0 ? 'active' : ''" @click="formData.taskType = 0">立即播放<img src="./img/bigHorn-xz.png" alt=""></div>
<div class="item" :class="formData.taskType == 1 ? 'active' : ''" @click="formData.taskType = 1"><img src="./img/bigHorn-xz.png" alt="">定时播放</div>
</div>
<div class="content" v-if="formData.taskType != 0">
<div class="item">
<div class="label">定时策略</div>
<div class="value" @click="selectClick('showCyclingType', cyclingTypeList)">
<span :class="formData.cyclingTypeName == '请选择' ? 'color-999' : ''">{{formData.cyclingTypeName}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
</div>
<div class="radio-content" v-if="formData.taskType != 0 && formData.cyclingType == 2">
<div class="title">播放天数</div>
<div class="mini-item" :class="item.isCheck ? 'mini-active' : ''" v-for="(item, index) in dayList" :key="index" @click="checkClick(index)">{{item.label}}</div>
</div>
<div class="content" v-if="formData.taskType != 0 && formData.cyclingType == 3">
<div class="item">
<div class="label">播放天数</div>
<div class="value">
<u-input type="text" placeholder="请输入" height="18" input-align="right" v-model="formData.broadcastDay" maxlength="4" />
</div>
</div>
</div>
<div class="content" v-if="formData.taskType != 0">
<div class="item">
<div class="label">开始日期</div>
<div class="value" @click="timeClick(true, 'showDate')">
<span :class="formData.startDate ? 'color-999' : ''">{{formData.startDate || '请选择'}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
<div class="item">
<div class="label">开始时间</div>
<div class="value" @click="timeClick(false, 'showSatrt')">
<span :class="formData.startTime ? 'color-999' : ''">{{formData.startTime || '请选择'}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
<div class="item">
<div class="label">结束时间</div>
<div class="value" @click="timeClick(false, 'showEnd')">
<span :class="formData.endTime ? 'color-999' : ''">{{formData.endTime || '请选择'}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
</div>
<div class="btn" @click="addConfirm">确认</div>
<u-select v-model="showSelect" :list="selectList" @confirm="confirm" label-name="dictName" value-name="dictValue"></u-select>
<u-picker v-model="showDateTime" mode="time" :params="params" @confirm="confirm"></u-picker>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
export default {
name: "addPlay",
data() {
return {
showSelect: false,
selectList: [],
showMedia: false,
mediaList: [],
showEquipment: false,
equipmentList: [],
showMessageLevel: false,
messageLevelList: [],
showCyclingType: false,
cyclingTypeList: [],
formData: {
mediaId: '',
mediaName: '请选择',
serialNo: '',
serialName: '请选择',
messageLevel: '',
messageLevelName: '请选择',
taskType: '0',
cyclingTypeName: '请选择',
cyclingType: '',
startDate: '',
startTime: '',
endTime: '',
broadcastDay: '',
cyclingDate: ''
},
dayList: [
{
isCheck: false,
value: 1,
label: '每周一'
},
{
isCheck: false,
value: 2,
label: '每周二'
},
{
isCheck: false,
value: 3,
label: '每周三'
},
{
isCheck: false,
value: 4,
label: '每周四'
},
{
isCheck: false,
value: 5,
label: '每周五'
},
{
isCheck: false,
value: 6,
label: '每周六'
},
{
isCheck: false,
value: 7,
label: '每周日'
}
],
showDateTime: false,
showDate: false,
showSatrt: false,
showEnd: false,
params: {
year: true,
month: true,
day: true,
hour: false,
minute: false,
second: false
},
}
},
components: {
AiBack
},
onLoad () {
uni.$on('choose', e => {
console.log(e)
this.formData.mediaId = e.mediaId
this.formData.mediaName = e.mediaName
})
},
methods: {
addConfirm() {
var cyclingDateList = []
this.dayList.map((item) => {
if(item.isCheck) {
cyclingDateList.push(item.value)
}
})
if(!this.formData.mediaId) {
return this.$u.toast('请选择播发内容')
}
if(!this.formData.serialNo) {
return this.$u.toast('请选择播放设备')
}
if(!this.formData.messageLevel) {
return this.$u.toast('播发级别')
}
//播放方式(定时播放)
if(this.formData.taskType != 0 && this.formData.startDate == '请选择') {
return this.$u.toast('请选择开始日期')
}
if(this.formData.taskType != 0 && this.formData.startTime == '请选择') {
return this.$u.toast('请选择开始时间')
}
if(this.formData.taskType != 0 && this.formData.endTime == '请选择') {
return this.$u.toast('请选择结束时间')
}
//播放方式(定时播放)定时策略(时长)
if(this.formData.taskType != 0 && this.formData.cyclingType == 3 && !this.formData.broadcastDay) {
return this.$u.toast('请输入播放天数')
}
//播放方式(定时播放)定时策略(自定义)
if(this.formData.taskType != 0 && this.formData.cyclingType == 2 && !cyclingDateList.length) {
return this.$u.toast('请选择播放天数')
}
if(this.formData.taskType != 0 && this.formData.cyclingType == 2) {
this.formData.cyclingDate = cyclingDateList.join(',')
}
this.formData.coverageType = '4'
this.$http.post(`/app/appzyvideobroadcast/play`, {...this.formData,}).then((res) => {
if (res.code == 0) {
this.$u.toast('提交成功')
setTimeout(() => {
uni.navigateBack()
}, 1000)
}
})
},
linkTo (path) {
uni.navigateTo({
url: path
})
},
confirm(e) {
if(this.showMedia) {
this.formData.mediaId = e[0].value
this.formData.mediaName = e[0].label
}
if(this.showEquipment) {
this.formData.serialNo = e[0].value
this.formData.serialName = e[0].label
}
if(this.showMessageLevel) {
this.formData.messageLevel = e[0].value
this.formData.messageLevelName = e[0].label
}
if(this.showCyclingType) {
this.formData.cyclingType = e[0].value
this.formData.cyclingTypeName = e[0].label
}
if(this.showDate) {
this.formData.startDate = e.year + '-' + e.month + '-' + e.day
}
if(this.showSatrt) {
var startTime = e.hour + ':' + e.minute + ':' + e.second
var myDate = new Date();
var time = myDate.getHours() + ':' + myDate.getMinutes() + ':' + myDate.getSeconds()
if (this.timeToSec(startTime) - this.timeToSec(time) > 0) {
this.formData.startTime = startTime
}else {
this.$u.toast('开始时间要大于当前时间')
}
}
if(this.showEnd) {
var endTime = e.hour + ':' + e.minute + ':' + e.second
console.log(this.timeToSec(endTime), this.timeToSec(this.formData.startTime))
if (this.timeToSec(endTime) - this.timeToSec(this.formData.startTime) > 0) {
this.formData.endTime = endTime
} else {
this.$u.toast('结束时间要大于开始时间')
}
}
this.init()
},
init() {
this.showMedia = false
this.showEquipment = false
this.showMessageLevel = false
this.showCyclingType = false
this.showDate = false
this.showSatrt = false
this.showEnd = false
},
selectClick(showType, list) {
this.showSelect = true
this[showType] = true
this.selectList = list
},
getMediaList() {
this.$http.post(`/app/appdlbresource/list?current=1&size=10000`).then((res) => {
if (res.code == 0) {
this.mediaList = []
if(res.data && res.data.records.length) {
res.data.records.map((item) => {
let info = {
dictName: item.name,
dictValue: item.id
}
this.mediaList.push(info)
})
}
}
})
},
getEquipmentList() {
this.$http.post(`/app/appdlbquipment/getDlbDeviceList?current=1&size=10000&devStatus=5&keyword=`).then((res) => {
if (res.code == 0) {
this.equipmentList = []
if(res.data && res.data.records.length) {
res.data.records.map((item) => {
let info = {
dictName: item.deviceName,
dictValue: item.serialNo
}
this.equipmentList.push(info)
})
}
}
})
},
checkClick(index) {
this.dayList[index].isCheck = !this.dayList[index].isCheck
},
timeClick(showYear, showType) {
this[showType] = true
this.showDateTime = true
if(showYear) {
this.params = {
year: true,
month: true,
day: true,
hour: false,
minute: false,
second: false
}
}else {
this.params = {
year: false,
month: false,
day: false,
hour: true,
minute: true,
second: true
}
}
},
timeToSec(time) {
var s = "";
var hour = time.split(":")[0];
var min = time.split(":")[1];
var second = time.split(":")[2];
s = Number(hour * 3600) + Number(min * 60) + Number(second)
return s;
},
},
created() {
this.$dict.load('dlbMessageUrgency', 'dlbBroadTaskType', 'dlbDyclingType').then(() => {
this.getMediaList()
this.getEquipmentList()
this.messageLevelList = this.$dict.getDict('dlbMessageUrgency')
this.cyclingTypeList = this.$dict.getDict('dlbDyclingType')
})
},
}
</script>
<style lang="scss" scoped>
.addPlay {
padding-bottom: 128px;
.content{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
padding: 34px 0;
font-size: 32px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
line-height: 44px;
border-bottom: 1px solid #ddd;
display: flex;
color: #333;
justify-content: space-between;
.label{
width: 198px;
font-size: 32px;
}
.value{
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 28px;
flex: 1;
padding-right: 32px;
max-width: calc(100% - 198px);
box-sizing: border-box;
text-align: right;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
img{
width: 32px;
height: 32px;
vertical-align: middle;
margin-left: 6px;
}
}
.color-999{
color: #999;
}
}
}
.radio-content{
padding: 34px 32px 38px;
border-bottom: 1px solid #ddd;
background-color: #fff;
.title{
font-size: 32px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #333;
line-height: 44px;
margin-bottom: 54px;
span{
font-size: 24px;
font-weight: 400;
}
}
.item{
display: inline-block;
width: 320px;
height: 112px;
line-height: 112px;
text-align: center;
background: #F5F5F5;
border-radius: 4px;
font-size: 30px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333;
img{
display: none;
}
}
.active{
background: #E7F1FE;
color: #1174FE;
position: relative;
img{
display: inline-block;
position: absolute;
bottom: 0;
right: 0;
width: 46px;
height: 46px;
}
}
.mar-r50 {
margin-right: 50px;
}
.mini-item{
display: inline-block;
width: 128px;
height: 72px;
line-height: 72px;
text-align: center;
background: #F9F9F9;
border-radius: 16px;
color: #333;
font-size: 28px;
margin-right: 58px;
margin-bottom: 32px;
}
.mini-item:nth-of-type(5) {
margin-right: 0;
}
.mini-active{
background: #F2F8FE;
border: 1px solid #89B2EE;
box-sizing: border-box;
}
}
.btn{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 112px;
line-height: 112px;
text-align: center;
background: #3975C6;
font-size: 32px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #FFFFFF;
}
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="bigHorn">
<div class="header">
<img src="./img/bigHorn-bg.png" alt="">
<div class="content">
<div class="item" @click="linkTo('./onlineList')">
<img src="./img/bigHorn-icon1@2x.png" alt="">
<div>在线设备</div>
<!-- <h2>1</h2> -->
</div>
<div class="item" @click="linkTo('./playList')">
<img src="./img/bigHorn-icon2@2x.png" alt="">
<div>播放记录</div>
</div>
<div class="item" @click="linkTo('./onlinePlayList')">
<img src="./img/bigHorn-icon3@2x.png" alt="">
<div>在播设备</div>
</div>
</div>
</div>
<div class="banner">
<div class="item" :class="item.bgClass" v-for="(item, index) in bannerList" :key="index" @click="linkTo(item.path)">
<h2>{{item.title}}</h2>
<div>{{item.text}}</div>
<img :src="item.imgUrl" alt="">
</div>
</div>
</div>
</template>
<script>
export default {
name: "bigHorn",
data() {
return {
bannerList: [
{
title: '素材播放',
text: '支持音频立即播发和定时播发',
imgUrl: require('./img/bigHorn-icon11@2x.png'),
path: '/pages/bigHorn/addPlay',
bgClass: 'bg-67A3F4'
},
{
title: '实时喊话',
text: '实时在线喊话,远程广播通知',
imgUrl: require('./img/bigHorn-icon22@2x.png'),
bgClass: 'bg-4ED5BB'
},
{
title: '音频录制',
text: '音频文件的录制',
path: '/pages/resourcesManage/addPlay?type=1',
imgUrl: require('./img/bigHorn-icon33@2x.png'),
bgClass: 'bg-E5B565'
},
{
title: '媒资管理',
path: '/pages/resourcesManage/resourcesManage',
text: '支持音频文件和录音内容添加',
imgUrl: require('./img/bigHorn-icon44@2x.png'),
bgClass: 'bg-F19661'
}
]
}
},
methods: {
linkTo(url) {
uni.navigateTo({url})
}
},
mounted() {
}
}
</script>
<style lang="scss" scoped>
.bigHorn {
.header{
position: relative;
img{
width: 100%;
height: 306px;
}
.content{
width: 686px;
padding: 40px 0;
background: #FFFFFF;
box-shadow: 0px 4px 8px 4px rgba(0, 0, 0, 0.06);
border-radius: 12px;
position: absolute;
top: 210px;
left: 50%;
transform: translateX(-50%);
display: flex;
.item{
text-align: center;
flex: 1;
img{
width: 64px;
height: 64px;
margin-bottom: 18px;
}
div{
font-size: 30px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
line-height: 42px;
margin-bottom: 10px;
}
h2{
font-size: 40px;
font-family: DINAlternate-Bold, DINAlternate;
font-weight: bold;
color: #333;
line-height: 48px;
}
}
}
}
.banner{
margin-top: 150px;
.item{
width: 686px;
height: 190px;
border-radius: 12px;
margin: 0 auto 24px auto;
padding: 40px 46px 0 80px;
box-sizing: border-box;
color: #FFF;
position: relative;
h2{
font-size: 42px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
line-height: 60px;
margin-bottom: 16px;
}
div{
font-size: 26px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #FFF;
line-height: 32px;
}
img{
width: 160px;
position: absolute;
top: 10px;
right: 30px;
}
}
.bg-67A3F4{
background: #67A3F4;
}
.bg-4ED5BB{
background: #4ED5BB;
}
.bg-E5B565{
background: #E5B565;
}
.bg-F19661{
background: #F19661;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,93 @@
<template>
<div class="onlineList">
<div class="record">
<div class="item" v-for="(item, index) in list" :key="index">
<img src="./img/bigHorn-lb@2x.png" alt="">
<div class="info">
<p>{{item.deviceName}}</p>
<span>{{item.areaName}}</span>
</div>
</div>
</div>
<ai-empty v-if="!list.length"></ai-empty>
<AiBack></AiBack>
</div>
</template>
<script>
import AiEmpty from '@/components/AiEmpty/AiEmpty'
import AiBack from "@/components/AiBack";
export default {
name: "onlineList",
data() {
return {
page: {current: 1, size: 10, total: 0},
list: []
}
},
components: {
AiEmpty,
AiBack
},
methods: {
getList() {
this.$http.post("/app/appdlbquipment/getDlbDeviceList", null, {
params: {...this.page, devStatus: 5}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
},
created() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.onlineList {
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
border-bottom: 1px solid #ddd;
display: flex;
padding: 12px 40px 16px 0;
img{
width: 48px;
height: 48px;
margin: 12px 16px 0 0;
}
.info{
width: calc(100% - 100px);
p{
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
line-height: 48px;
margin-bottom: 12px;
}
span{
font-size: 22px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #999;
line-height: 32px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div class="onlinePlayList">
<div class="record">
<div class="item" v-for="(item, index) in list" :key="index">
<img src="./img/bigHorn-lb@2x.png" alt="">
<div class="info">
<p>{{item.deviceName}}</p>
<span>{{item.createTime}}</span><br />
<span>{{item.name}}</span>
</div>
</div>
</div>
<ai-empty v-if="!list.length"></ai-empty>
<AiBack></AiBack>
</div>
</template>
<script>
import AiEmpty from '@/components/AiEmpty/AiEmpty'
import AiBack from "@/components/AiBack";
export default {
name: "onlinePlayList",
data() {
return {
page: {current: 1, size: 10, total: 0},
list: []
}
},
components: {
AiEmpty,
AiBack
},
methods: {
getList() {
this.$http.post("/app/appdlbquipment/getDlbDeviceList", null, {
params: {...this.page, devStatus: 1}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
},
created() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.onlinePlayList {
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
border-bottom: 1px solid #ddd;
display: flex;
padding: 12px 40px 16px 0;
img{
width: 48px;
height: 48px;
margin: 12px 16px 0 0;
}
.info{
width: calc(100% - 100px);
p{
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
line-height: 48px;
margin-bottom: 12px;
}
span{
font-size: 22px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #999;
line-height: 32px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div class="playList">
<div class="title">
<span>播放记录</span>
<span>操作</span>
</div>
<div class="record">
<div class="item" v-for="(item, index) in list" :key="index">
<div class="info">
<p>{{item.messageName}}</p>
<span>{{item.createTime}}</span><br />
<span>{{item.deviceName}}</span>
</div>
<div class="btn bg-3975C6" v-if="item.broadcastStatus == 0 || item.broadcastStatus == 1 || item.broadcastStatus == 2" @click="cancel(item.broadcastId)">撤销</div>
<div class="btn bg-AFD0FC" v-if="item.broadcastStatus == 6">已取消</div>
</div>
</div>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
export default {
name: "playList",
data() {
return {
page: {current: 1, size: 10, total: 0},
list: []
}
},
components: {
AiBack
},
methods: {
getList() {
this.$http.post("/app/appzyvideobroadcast/getBroadcastRecords", null, {
params: {...this.page}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
cancel(id) {
this.$confirm('确定撤回该广播?').then(() => {
this.$http.post(`/app/appzyvideobroadcast/getBroadcastRecall?broadcastId=${id}`).then((res) => {
if (res.code == 0) {
this.$u.toast('撤回成功!')
this.getList()
}
})
})
},
},
created() {
this.$dict.load('dlbBroadcastStatus').then(() => {
this.getList()
})
}
}
</script>
<style lang="scss" scoped>
.playList {
.title{
width: 100%;
height: 88px;
line-height: 88px;
background: #FFF;
padding: 0 80px;
display: flex;
justify-content: space-between;
color: #333;
font-size: 34px;
box-sizing: border-box;
border-bottom: 1px solid #ddd;
}
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
padding: 12px 40px 16px 0;
box-sizing: border-box;
.info{
width: 480px;
margin-right: 40px;
word-break: break-all;
p{
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
line-height: 48px;
margin-bottom: 12px;
}
span{
font-size: 22px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #999;
line-height: 32px;
}
}
.btn{
width: 154px;
height: 60px;
border-radius: 8px;
text-align: center;
line-height: 60px;
font-size: 30px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #FFF;
margin-top: 18px;
}
.bg-3975C6{
background: #3975C6;
}
.bg-AFD0FC{
background: #AFD0FC;
}
}
}
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<div class="selectEquipment">
<div class="search">
<div class="search-bg">
<img src="./img/search-icon.png" alt="">
<u-input v-model="value" type="text" placeholder="搜索设备名称" class="search-input" height="18" />
</div>
</div>
<div class="record">
<div class="item">
<img src="./img/cir.png" alt="" class="check-img">
<img src="./img/lb@2x.png" alt="" class="voice-img">
<div class="info">
<div class="text">
<p>村头大喇叭</p>
<span>刘家河居委会</span>
</div>
<div class="status">在线</div>
</div>
</div>
<div class="item">
<img src="./img/cir.png" alt="" class="check-img">
<img src="./img/lb@2x.png" alt="" class="voice-img">
<div class="info">
<div class="text">
<p>村头大喇叭</p>
<span>刘家河居委会</span>
</div>
<div class="status">在线</div>
</div>
</div>
</div>
<div class="btn">
<div>确定选择</div>
</div>
</div>
</template>
<script>
export default {
name: "selectEquipment",
data() {
return {
value: ''
}
},
methods: {
},
mounted() {
}
}
</script>
<style lang="scss" scoped>
.selectEquipment {
padding-bottom: 128px;
.search{
width: 100%;
height: 104px;
background: #FFF;
margin-bottom: 4px;
padding: 20px 32px;
box-sizing: border-box;
.search-bg{
width: 686px;
height: 64px;
padding: 14px 0;
box-sizing: border-box;
background: #F5F5F5;
border-radius: 32px;
position: relative;
img{
width: 32px;
height: 32px;
position: absolute;
top: 16px;
left: 32px;
}
.search-input{
width: 590px;
height: 36px;
line-height: 36px;
font-size: 28px;
margin-left: 70px;
}
}
}
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
display: flex;
border-bottom: 1px solid #ddd;
.check-img{
width: 32px;
height: 32px;
margin: 32px 32px 0 0;
}
.voice-img{
width: 48px;
height: 48px;
margin: 28px 16px 0 0;
}
.info{
width: calc(100% - 148px);
padding: 18px 0;
line-height: 44px;
font-size: 34px;
display: flex;
justify-content: space-between;
.text{
p{
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
span{
font-size: 26px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #999;
line-height: 36px;
}
}
.status{
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #4E8EEE;
line-height: 48px;
}
}
}
}
.btn{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 128px;
background: #FFF;
border-top: 1px solid #ddd;
padding: 24px 32px 24px 0;
box-sizing: border-box;
div{
width: 192px;
height: 80px;
line-height: 80px;
text-align: center;
background: #3975C6;
border-radius: 4px;
color: #fff;
font-size: 32px;
float: right;
}
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="selectMp3">
<div class="record">
<div class="item">
<img src="./img/cir.png" alt="">
<div class="info">
<p>村头大喇叭</p>
</div>
</div>
</div>
<div class="btn">
<div>确定选择</div>
</div>
</div>
</template>
<script>
export default {
name: "selectMp3",
data() {
return {
}
},
methods: {
},
mounted() {
}
}
</script>
<style lang="scss" scoped>
.selectMp3 {
padding-bottom: 128px;
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
display: flex;
img{
width: 32px;
height: 32px;
margin: 40px 16px 0 0;
}
.info{
width: calc(100% - 60px);
padding-bottom: 16px;
padding: 34px 0;
line-height: 44px;
border-bottom: 1px solid #ddd;
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
margin-left: 16px;
}
}
}
.btn{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 128px;
background: #FFF;
border-top: 1px solid #ddd;
padding: 24px 32px 24px 0;
box-sizing: border-box;
div{
width: 192px;
height: 80px;
line-height: 80px;
text-align: center;
background: #3975C6;
border-radius: 4px;
color: #fff;
font-size: 32px;
float: right;
}
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<div class="casuallyask">
<div class="banner">
<picker :range="dictList" @change="bindPickerChange" range-key="dictName">
<div class="picker">
{{ askType }}
<u-icon
name="arrow-down"
:custom-style="{ margin: '0 5px' }"
></u-icon>
</div>
</picker>
<u-search
placeholder="请输入标题"
:show-action="false"
v-model="keyword"
@search="onSearch"
/>
</div>
<u-tabs
class="nav"
:list="tabs"
:is-scroll="false"
:current="currentType"
font-size="32"
bar-width="192"
height="96"
@change="handleTabClick"
></u-tabs>
<div class="body" v-if="eventList.length !== 0">
<div
class="content"
v-for="(item, index) in eventList"
:key="index"
@click="detail(item)"
>
<u-row>
<div>
<div class="top">
<text :class="item.type == '0' ? 'active' : 'noactive'">
{{ $dict.getLabel('leaveMessageType', item.type) }}
</text>
<!-- -->
<text class="areaName" v-if="item.areaName">{{ item.areaName }}</text>
<!-- 问题 -->
<text class="title">{{ item.title }}</text>
</div>
<div class="cont">
<span class="name_time">留言人:</span>
<text class="text_c"> {{ item.leaveName }}</text>
</div>
<div class="cont">
<text class="name_time">留言时间:</text>
<text class="text_c"> {{ item.createTime }}</text>
</div>
</div>
</u-row>
</div>
</div>
<AiEmpty v-else/>
</div>
</template>
<script>
import AiEmpty from '../../components/AiEmpty/AiEmpty'
import URow from '../../uview/components/u-row/u-row.vue'
export default {
name: 'casuallyAsk',
// 组件
components: {URow, AiEmpty},
props: {},
data() {
return {
page: {current: 1, size: 10, total: 0},
dictList: [], // 字典
index: 0, // 留言类型 0投诉 1咨询 2建议
keyword: '',
askType: '提问类型',
askIndex: '',
currentType: 0, // 默认value的值(0待我回复)(1已回复)(2处理完成)
eventList: [] // 数据列表
// temp: { '0': '投诉', '1': '建议', '2': '咨询' }
}
},
// 计算
computed: {
tabs() {
return [
{name: '待我回复', value: 0},
{name: '我已回复', value: 1},
{name: '处理完成', value: 2}
]
}
},
// 实例创建后
onShow() {
this.$dict.load('leaveMessageType').then(() => {
this.dictList = this.$dict.getDict('leaveMessageType')
this.getList()
})
},
// 方法
methods: {
// 点击 value的值(0待我回复)(1已回复)(2处理完成)
handleTabClick(i) {
this.currentType = i
this.getList()
},
getList() {
this.$http.post(`/app/appleavemessage/list`, null, {
params: {
...this.page,
title: this.keyword,
status: this.currentType,
type: this.askIndex
}
}).then(res => {
if (res?.data) {
this.page.total = res.data.total
if (this.page.current > 1)
this.eventList = [...this.eventList, res.data.records]
else this.eventList = res.data.records
}
})
},
detail(item) {
uni.navigateTo({
url: `/pages/casuallyask/casuallyaskDetail?id=${item.id}&type=${item.type}`
// url: `/pages/casuallyask/casuallyaskDetail?id=${item.id}`
})
},
// 提问类型
bindPickerChange(e) {
let index = e.detail.value
this.askType = this.dictList[index].dictName
this.askIndex = index
this.getList()
},
onSearch(e) {
this.keyword = e
this.getList()
}
},
onReachBottom() {
if (this.eventList.length < this.page.total) {
this.page.current++
this.getList()
}
}
}
</script>
<style scoped lang="scss">
.casuallyask {
min-height: 100%;
background: #f5f5f5;
padding-top: 64px;
.banner {
position: fixed;
top: 0;
width: 100%;
background-color: #fff;
height: 80px;
line-height: 80px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 30px 0 30px;
.picker {
font-size: 30px;
font-weight: 500;
color: #333333;
margin-right: 8px;
.u-icon-wrap {
margin-left: 25px;
}
}
}
.nav {
position: fixed;
top: 80px;
background-color: #ffffff;
height: 96px;
padding-top: 8px;
width: 100%;
}
.body {
padding: 114px 0 0 0;
.content {
background-color: #ffffff;
box-sizing: border-box;
padding: 34px 32px;
margin: 32px 20px;
.top {
display: flex;
align-items: center;
width: 100%;
.typeBox {
}
.noactive {
text-align: center;
color: #2266ff;
width: 70px;
height: 44px;
line-height: 44px;
border-radius: 8px;
background-color: #e8efff;
}
.active {
text-align: center;
color: #ff4466;
line-height: 44px;
width: 70px;
height: 44px;
border-radius: 8px;
background-color: #ffebef;
}
.areaName {
display: inline-block;
color: #333;
font-size: 32px;
margin-left: 15px;
}
.title {
color: #333;
font-size: 32px;
}
}
.cont {
margin: 10px 0;
.name_time {
display: inline-block;
width: 150px;
height: 24px;
color: #999;
font-size: 30px;
// font-weight: 800;
}
.text_c {
color: #343d65;
font-size: 30px;
// font-weight: 800;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,442 @@
<template>
<div class="casuallyaskDetail">
<div class="detail">
<div class="headerTitle" flex>
<div class="temp">
[{{ $dict.getLabel('leaveMessageType', detail.type) }}]
</div>
<span class="areaName">{{ detail.areaName }}</span>
<span>{{ detail.title }}</span>
</div>
<div class="leaveName_leavePhone">
<!-- 顶部圆形头像 -->
<span class="icon">{{ detail.headPortrait || $formatName(detail.leaveName) }}</span>
<span class="leaveName">{{ detail.leaveName }}</span>
<span class="leavePhone" v-if="detail.leavePhone">({{ detail.leavePhone }})</span>
</div>
<!-- 编号 -->
<div class="info-item">
<span class="label">
<u-icon name="order"></u-icon>
</span>
<span class="value">{{ detail.msgCode }}</span>
</div>
<!-- 日期 -->
<div class="info-item">
<span class="label">
<u-icon name="clock"></u-icon>
</span>
<span class="value">{{ detail.createTime }}</span>
</div>
<!-- 进度-->
<div class="info-item">
<span class="label"><u-icon name="tags"/></span>
<text class="status">{{
$dict.getLabel('leaveMessageStatus', detail.status)
}}
</text>
</div>
<div class="content_text_img">
<!-- 提问内容 -->
<div class="content_text">
{{ detail.content }}
</div>
<!-- 提问内容的图片 -->
<div class="imageList">
<ai-image preview :src="items.url" alt="" v-for="(items, i) in imgList" :key="i"/>
</div>
</div>
<div class="reply_content" v-if="detail.status == 1 || detail.status == 2">
<div class="reply_title">
<img src="https://cdn.cunwuyun.cn/img/dialogue.svg" alt=""/>
<p>沟通记录</p>
</div>
<div class="reply_list">
<div class="item" v-for="(item, index) in appLeaveMessageReplyList" :key="index">
<div class="item_top">
<div class="item_left">
<div class="icon">
{{ detail.headPortrait || $formatName(item.createUserName) }}
</div>
<div class="name fill">
<div class="createUserName_createUnitName">
<span v-if="item.createUserId == user.id" class="reply_font">我的回复</span>
<template v-else>
<span class="createUserName">{{ item.createUserName }}&nbsp;</span>
<span class="createUserName" v-if="item.createUserPhone">({{ item.createUserPhone }})</span>
</template>
</div>
<div flex v-if="item.createUserId != user.id">
<!-- 回复单位 -->
<span class="createUnitName">{{ item.createUnitName }}</span>
<span class="reply">回复</span>
</div>
</div>
</div>
<div class="item_right">{{ item.createTime }}</div>
</div>
<!-- 回复内容 -->
<div class="myreply_con">
<div class="myreply_text">
{{ item.content }}
</div>
<!-- 回复图片 -->
<div class="imageList">
<ai-image v-for="img in item.images" :key="img.id" preview :src="img.url"/>
</div>
<!-- -->
</div>
</div>
</div>
</div>
</div>
<div class="close_reply" v-if="detail.status == 0 || detail.status == 1">
<div class="btn" @click="close">关闭留言</div>
<div class="btn reply" @click="reply">回复</div>
</div>
<u-modal
v-model="show"
:content="content"
cancel-color="#2979ff"
title=""
:async-close="true"
:show-cancel-button="true"
@confirm="handleCloseMessage"
@cancel="show = false"/>
<back/>
</div>
</template>
<script>
import back from '../../components/AiBack'
import {mapState} from 'vuex'
import AiImage from "../../components/AiImage";
import UImage from "../../uview/components/u-image/u-image";
export default {
name: 'casuallyaskDetail',
components: {UImage, AiImage, back},
props: {},
computed: {
...mapState(['user'])
},
onLoad(options) {
this.getDetail(options.id)
},
onShow() {
this.$dict.load('leaveMessageStatus', 'leaveMessageType')
},
data() {
return {
objdie: '', // id
appLeaveMessageReplyList: [], //遍历得到的数据
show: false, // 默认关闭模态框
content: '关闭留言后,双方都无法再进行回复,是否确定关闭本次留言?',
msgCode: null,
imgList: [],
detail: {}
// images:[]
}
},
methods: {
getDetail(id = this.detail.id) {
this.$http.post(`/app/appleavemessage/queryDetailById?id=${id}`,).then(res => {
if (res?.data) {
this.detail = res.data
this.imgList = JSON.parse(this.detail.images)
this.appLeaveMessageReplyList = res.data.appLeaveMessageReplyList.map(item => ({
...item,
images: JSON.parse(item.images).map(e => ({url: e.url || e?.file?.accessUrl, id: e.file?.id}))
}))
}
})
},
// 关闭留言
close() {
this.show = true
this.getDetail()
},
// 确定关闭留言
handleCloseMessage() {
this.$http.post(`/app/appleavemessage/release?id=${this.objdie}&status=2`).then(res => {
if (res?.code == 0) {
this.getDetail()
uni.navigateTo({url: `./closemsg?flag=true`})
}
}).catch(() => {
uni.navigateTo({url: `./closemsg?flag=false`})
})
this.show = false
},
// 去回复
reply() {
uni.navigateTo({url: `./reply?msgCode=${this.detail.msgCode}`})
}
},
}
</script>
<style lang="scss" scoped>
.casuallyaskDetail {
padding-bottom: 80px;
.detail {
padding: 15px 32px 112px 32px;
background-color: #fff;
font-size: 30px;
font-weight: 400;
color: #343D65;
.headerTitle {
font-size: 40px;
font-weight: bold;
color: #333;
line-height: 64px;
letter-spacing: 2px;
flex-wrap: wrap;
}
.leaveName_leavePhone {
height: 56px;
margin: 20px 0 36px 0;
.icon {
display: inline-block;
width: 56px;
height: 56px;
line-height: 56px;
border-radius: 50%;
background-color: #2266ff;
color: #fff;
font-size: 22px;
vertical-align: middle;
text-align: center;
}
.leaveName {
// display: block;
margin-left: 20px;
color: #343d65;
}
// .leavePhone {
// }
}
.info-item {
height: 40px;
line-height: 40px;
font-size: 28px;
margin-bottom: 8px;
.label {
display: inline-block;
}
.value {
display: inline-block;
margin-left: 20px;
}
.status {
display: inline-block;
margin-left: 20px;
// color: #999;
}
}
.content_text_img {
margin-top: 64px;
max-height: 700px;
color: #000;
font-size: 32px;
// font-weight: 800;
overflow: hidden;
.content_text {
width: 100%;
margin-bottom: 12px;
}
}
.reply_content {
margin-top: 25px;
.reply_title {
margin-top: 25px;
width: 225px;
height: 45px;
line-height: 45px;
img {
vertical-align: middle;
}
p {
display: inline-block;
color: #333;
// font-weight: 800;
margin-left: 10px;
}
}
.reply_list {
.item {
.item_top {
display: flex;
justify-content: space-between;
margin-top: 10px;
.item_left {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
.icon {
display: block;
flex-shrink: 0;
width: 64px;
height: 64px;
line-height: 64px;
border-radius: 50%;
background-color: #2266ff;
color: #fff;
font-size: 23px;
text-align: center;
margin-right: 10px;
}
.name {
display: flex;
flex-direction: column;
.createUnitName {
color: #135ab8;
margin-right: 8px;
}
.createUserName_createUnitName {
width: 100%;
display: flex;
.reply_font {
width: 120px;
color: #333333;
font-size: 28px;
}
.createUserName {
margin-bottom: 5px;
color: #135ab8;
}
}
}
}
.item_right {
font-size: 26px;
color: #999;
}
}
.myreply_con {
width: 570px;
background-color: #f3f6f9;
margin: 24px 0 0 80px;
padding: 14px;
.myreply_text {
overflow: auto;
word-wrap: break-word;
word-break: break-all;
}
.imageList {
display: flex;
flex-wrap: wrap;
.AiImage {
margin: 0 12px 12px 0;
}
}
}
}
.no_yes_reply {
.con_title {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 16px;
height: 64px;
line-height: 64px;
.icon {
display: inline-block;
width: 60px;
height: 60px;
line-height: 60px;
border-radius: 50%;
background-color: #2266ff;
color: #fff;
font-size: 23px;
vertical-align: middle;
text-align: center;
// font-weight: 800;
}
.my_reply {
width: 370px;
}
.right {
color: #999;
}
}
.myreply_content {
width: 606px;
height: 460px;
background-color: #f3f6f9;
margin: 24px 0 0 62px;
padding: 16px;
}
}
}
}
.no_more {
padding: 44px 0;
color: #999;
font-size: 24px;
text-align: center;
}
}
.close_reply {
position: fixed;
bottom: 0;
display: flex;
height: 112px;
width: 100%;
border-top: 1px solid #ddd;
z-index: 999;
background: #fff;
.btn {
flex: 1;
min-width: 0;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
&.reply {
background-color: #1365dd;
color: #fff;
}
}
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="closemsg">
<img :src="imgSrc" alt="" />
<text>{{ text }}</text>
<u-button
type="primary"
:custom-style="{ width: '100%', borderRadius: '4px', marginTop: '48px' }"
@click="goBack"
>{{ btnText }}</u-button
>
<back></back>
</div>
</template>
<script>
import back from '../../components/AiBack'
export default {
name: 'CloseMsg',
components: { back },
data() {
return {
flag: true
}
},
onLoad(val) {
if (val.flag) {
this.flag = val.flag
}
},
methods: {
goBack() {
uni.navigateBack({
// url: `/pages/casuallyask/casuallyask`
delta: 1
})
}
},
computed: {
text() {
return this.flag ? '关闭留言成功!' : '关闭留言失败'
},
btnText() {
return this.flag ? '确定' : '查看详情'
},
imgSrc() {
return this.flag
? this.imgOtherUrl + 'kztcg.png'
: this.imgOtherUrl + 'kztsb.png'
}
}
}
</script>
<style lang="scss" scoped>
.closemsg {
min-height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
padding: 96px;
img {
width: 192px;
height: 192px;
}
text {
font-size: 36px;
font-weight: 800;
color: #333333;
line-height: 50px;
display: flex;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<div class="reply">
<div class="reply_content">
<span class="icon">*</span>
<text class="msgfont">回复内容</text>
<textarea :maxlength="500" placeholder="请输入内容(500字以内)" v-model="msg" class="textarea"></textarea>
<!-- <u-input
type="textarea"
height="50"
:auto-height="false"
placeholder="请输入内容500字以内"
placeholder-style="color:#999;"
maxlength="500"
v-model="msg"
class="textarea"
/> -->
</div>
<div class="reply_img">
<text class="img">图片资料</text>
<text class="img_text">(最多9张)</text>
<div class="upload">
<div class="info">
<ai-uploader multiple @data="data" @change="change" :limit="9" action="/admin/file/add2"></ai-uploader>
</div>
</div>
</div>
<ai-back/>
<div class="submit">
<button class="btn" @click="btn">提交</button>
</div>
</div>
</template>
<script>
import AiUploader from '../../components/AiUploader'
import AiBack from "../../components/AiBack";
export default {
// name: '',
// 组件
components: {AiBack, AiUploader},
props: {},
data() {
return {
msg: '',
files: [],
msgCode: '',
}
},
onLoad(options) {
this.msgCode = options.msgCode
},
// 计算
computed: {},
// 监听
watch: {},
// 实例创建后
onShow() {
this.$dict.load('leaveMessageType').then(() => {
this.dictList = this.$dict.getDict('leaveMessageType')
})
},
// 实例渲染后
mounted() {
},
// 方法
methods: {
data(e) {
this.files.push(e)
},
// selectEventType(selecteds) {
// this.eventType = selecteds?.[0]?.value
// },
change(e) {
this.files = e
},
btn() {
if (this.msg == '') {
return uni.showToast({
title: '请输入留言内容',
icon: 'none',
})
}
this.$http
.post(`/app/appleavemessagereply/addOrUpdate`, {
images: JSON.stringify(this.files),
content: this.msg,
msgCode: this.msgCode,
userType: '1',
}).then(res => {
if (res?.code == 0) {
uni.navigateTo({
url: `/pages/casuallyask/truemsg?flag=1`,
})
}
}).catch(err => {
uni.navigateTo({
url: `/pages/casuallyask/truemsg?flag=0`,
})
this.$u.toast(err || '网络异常')
})
},
},
}
</script>
<style scoped lang="scss">
.reply {
width: 100%;
// height: 100%;
.reply_content {
height: 288px;
padding: 0 20px 0 20px;
background-color: #fff;
.icon {
display: inline-block;
width: 16px;
height: 44px;
line-height: 44px;
font-size: 42px;
vertical-align: middle;
color: red;
margin-top: 10px;
}
.msgfont {
// overflow: auto;
// word-wrap: break-word;
// word-break: break-all;
display: inline-block;
width: 226px;
height: 44px;
margin-top: 34px;
line-height: 44px;
font-size: 32px;
// font-weight: 800;
margin-left: 10px;
}
.textarea {
height: 190px;
width: 700px;
// overflow: auto;
// word-wrap: break-word;
// word-break: break-all;
}
}
.reply_img {
margin-top: 20px;
background-color: #fff;
padding: 0 20px;
box-sizing: border-box;
.img {
display: inline-block;
margin-top: 20px;
font-size: 32px;
// font-weight: 800;
}
.img_text {
font-size: 32px;
color: #999;
margin-left: 10px;
}
.upload {
width: 100%;
margin-top: 10px;
padding: 12px 12px 12px 0;
box-sizing: border-box;
.info {
width: 100%;
text-align: center;
}
::v-deep .ai-uploader .fileList .default {
width: 160px;
height: 160px;
}
}
}
.submit {
padding: 50px 32px;
.btn {
background-color: #1365dd;
color: #fff;
font-size: 36px;
}
}
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div class="closemsg">
<img :src="imgSrc" alt=""/>
<text>{{ text }}</text>
<u-button
type="primary"
:custom-style="{ width: '100%', borderRadius: '4px', marginTop: '48px' }"
@click="goBack"
>{{ btnText }}
</u-button>
<back></back>
</div>
</template>
<script>
import back from '../../components/AiBack'
export default {
name: 'CloseMsg',
components: {back},
data() {
return {
flag: true
}
},
onLoad(val) {
this.flag = val?.flag == 1
},
methods: {
goBack() {
uni.navigateBack({
delta: 2
})
}
},
computed: {
text() {
return this.flag ? '提交成功!' : '处理失败'
},
btnText() {
return this.flag ? '查看详情' : '我知道了'
},
imgSrc() {
return this.flag
? this.imgOtherUrl + 'kztcg.png'
: this.imgOtherUrl + 'kztsb.png'
}
}
}
</script>
<style lang="scss" scoped>
.closemsg {
min-height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
padding: 96px;
img {
width: 192px;
height: 192px;
}
text {
font-size: 36px;
font-weight: 800;
color: #333333;
line-height: 50px;
display: flex;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<div class="approval">
<div class="card">
<header>批示意见</header>
<textarea placeholder="请输入批示意见" v-model.trim="description" maxlength="200"></textarea>
<u-row justify="between">
<span @click="description=''">清空内容</span>
<span>{{description.length || 0}}/200</span>
</u-row>
</div>
<ai-back/>
<u-button type="primary" @click="submit">提交</u-button>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
export default {
name: "approval",
components: {AiBack},
data() {
return {
id: null,
description: ""
}
},
onLoad(opt) {
this.id = opt.id
},
methods: {
submit() {
this.$http.post("/app/appofficialdocumentinfo/instructionById", null, {
params: {
id: this.id,
description: this.description
}
}).then(res => {
if (res.code == 0) {
this.$u.toast("批示成功")
uni.navigateBack()
}
})
}
}
}
</script>
<style lang="scss" scoped>
.approval {
background: #F5F5F5;
.card {
background-color: #FFFFFF;
box-sizing: border-box;
padding: 32px;
header {
font-size: 32px;
color: #333333;
margin-bottom: 16px;
font-weight: bold;
}
textarea {
width: 100%;
}
span:first-child {
font-size: 28px;
color: #1365DD;
}
span:last-child {
font-size: 24px;
color: #999999;
}
}
.u-btn {
width: 100%;
height: 112px !important;
font-size: 32px;
color: #FFFFFF;
position: fixed;
left: 0;
bottom: 0;
background: #197DF0 !important;
}
}
</style>

View File

@@ -0,0 +1,432 @@
<template>
<div class="detail">
<template v-if="!userSelect">
<div class="card">
<header>{{detail.documentName}}</header>
<u-gap height="16"></u-gap>
<u-row>
<span>公文编号</span>
<span>{{detail.documentCode}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>公文类型</span>
<span>{{$dict.getLabel("officialDocumentName",detail.documentType)}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>紧急程度</span>
<span :style="{color:$dict.getColor('documentEmergencyLevel',detail.emergencyLevel)}">{{$dict.getLabel("documentEmergencyLevel",detail.emergencyLevel)}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>发文机关</span>
<span>{{detail.issuingUnit}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>发文字号</span>
<span>{{detail.issuingFont}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>签发人</span>
<span>{{detail.signer}}</span>
</u-row>
<u-gap height="16"></u-gap>
<img v-if="detail.confidentialityLevel" :src="$cdn + tag(detail.confidentialityLevel)" alt="">
</div>
<div class="card" style="margin-bottom: 0;padding-top: 0">
<div class="label">备注</div>
<span>{{detail.remark}}</span>
</div>
<div class="card" style="padding-top: 0" v-if="detail.files && detail.files.length">
<div class="label">相关附件</div>
<div class="file" v-for="(item,index) in detail.files" :key="index" @click="preFile(item)">
<u-row justify="between">
<label class="left">
<img :src="$cdn + 'common/appendix.png'" alt="">
<span>{{item.fileName}}.{{item.postfix}}</span>
</label>
<span>{{(item.size/1024).toFixed(2)}}KB</span>
</u-row>
</div>
</div>
<div class="card">
<div class="label" style="96px;">{{detail.readType ==0 ? "流转信息" : "传阅情况"}}
<em>({{$dict.getLabel("documentStatus",detail.status)}})</em></div>
<div class="progress">
<div class="item" v-for="(item,index) in detail.flowUsers" :key="index">
<div class="avatar">{{item.flowUserName && item.flowUserName.substr(-2)}}</div>
<div class="right">
<u-row justify="between">
<text class="status" :style="{color:item.readStatus==1?'#FF8822':'#1365DD'}">{{$dict.getLabel(detail.readType ==1 ? "readingStatus" :
"documentFlowStatus",detail.readType ==0 ? item.flowStatus : item.readStatus)}}
</text>
<text class="date">{{item.flowTime}}</text>
</u-row>
<u-row justify="between">
<text class="name">{{item.flowUserName}}</text>
</u-row>
<u-row justify="between">
<text class="note">{{item.description}}</text>
</u-row>
</div>
</div>
</div>
</div>
<div class="footer" v-if="detail.flowRight==1 && detail.readType==0">
<div @click="handleClick(0)">批示</div>
<div @click="handleClick(1)">流转</div>
</div>
<div class="footer" v-if="detail.readType==1 && detail.flowRight==1" @click="read" style="background-color: #1365DD;color: #FFFFFF">我已阅完</div>
</template>
<AiSelectEnterprise :visible.sync="userSelect" v-if="userSelect" :multiple="false"
@change="change"></AiSelectEnterprise>
<AiBack v-if="!userSelect"/>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
import AiSelectEnterprise from "../../../components/AiSelectEnterprise/AiSelectEnterprise";
import {mapActions} from "vuex";
export default {
name: "detail",
components: {AiBack, AiSelectEnterprise},
data() {
return {
id: null,
detail: {},
userSelect: false,
}
},
onLoad(opt) {
this.$dict.load("officialDocumentName", "documentEmergencyLevel", "documentStatus", "readingStatus", "documentFlowStatus")
this.id = opt.id
},
methods: {
...mapActions(['previewFile', 'injectJWeixin']),
read() {
this.$http.post("/app/appofficialdocumentinfo/readById", null, {
params: {
id: this.id
}
}).then(res => {
if (res.code == 0) {
this.$u.toast("已阅读")
this.getDetail()
}
})
},
preFile(e) {
if([".jpg",".png",".gif"].includes(e.postfix.toLowerCase())){
uni.previewImage({
current: e.url,
urls: [e.url]
})
}else {
this.previewFile({...e})
}
},
change(e) {
this.$http.post("/app/appofficialdocumentinfo/flowById", null, {
params: {
flowUserId: e[0].id,
flowUserName: e[0].name,
id: this.id,
avatar: e[0].avatar,
flag: 0
}
}).then(res => {
if (res.code == 0) {
this.$u.toast("流转成功")
this.getDetail()
}
})
},
tag(status) {
return {
"0": "common/mm.png",
"1": "common/jm.png",
"2": "common/tm.png"
}[status]
},
getDetail() {
this.$http.post("/app/appofficialdocumentinfo/queryDetailById", null, {
params: {
id: this.id,
flag: 1
}
}).then(res => {
if (res && res.data) {
this.detail = res.data
}
})
},
handleClick(status) {
if (status == 0) {
uni.navigateTo({
url: "/pages/documentFlow/components/approval?id=" + this.id
})
} else {
this.userSelect = true
}
}
},
onShow() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.detail {
min-height: 100%;
background-color: #F5F5F5;
padding-bottom: 140px;
position: relative;
.card {
background-color: #FFFFFF;
margin-bottom: 8px;
box-sizing: border-box;
padding: 16px 32px;
position: relative;
header {
font-size: 40px;
font-weight: 600;
color: #333333;
line-height: 64px;
letter-spacing: 1px;
}
.u-row {
& > div {
background-color: #2266FF;
border-radius: 50%;
text-align: center;
font-size: 22px;
font-weight: bold;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
& > span:first-child {
font-size: 30px;
color: #999999;;
line-height: 48px;
}
& > span:last-child {
font-size: 30px;
color: #343D65;
margin-left: 16px;
line-height: 48px;
}
}
& > img {
width: 190px;
height: 190px;
position: absolute;
right: 0;
top: 74px;
}
& > span {
font-size: 32px;
color: #333333;
line-height: 48px;
letter-spacing: 1px;
display: inline-block;
}
.label {
height: 80px;
font-size: 32px;
color: #333333;
display: flex;
align-items: center;
margin-bottom: 16px;
& > em {
font-style: normal;
font-size: 32px;
color: #1365DD;
}
}
.file {
height: 128px;
background: #FFFFFF;
border-radius: 8px;
border: 1px solid #CCCCCC;
box-sizing: border-box;
padding: 0 16px;
margin-bottom: 32px;
& > .u-row {
height: 100%;
.left {
width: 500px;
display: flex;
align-items: center;
& > img {
flex-shrink: 0;
width: 96px;
height: 96px;
}
& > span {
font-size: 32px;
color: #333333;
display: inline-block;
line-height: 44px;
overflow: hidden;
text-overflow: ellipsis;
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
}
}
& > span {
font-size: 28px;
color: #999999;
}
}
}
.active {
background-color: #F3F6F9;
}
.progress {
margin-top: 8px;
.item {
display: flex;
align-items: center;
position: relative;
min-height: 136px;
margin-bottom: 80px;
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #2266FF;
font-size: 28px;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.right {
width: 100%;
display: flex;
flex-direction: column;
& > .u-row {
margin-left: 40px;
.status {
font-size: 32px;
color: #333333;
}
.date {
font-size: 28px;
color: #999999;
}
.name {
font-size: 28px;
color: #666666;
margin: 8px 0;
}
.note {
font-size: 28px;
color: #343D65;
}
}
}
&:after {
content: "";
width: 4px;
height: 100%;
background-color: #EEEEEE;
position: absolute;
left: 40px;
top: 112px;
}
&:last-child:after {
display: none;
}
}
}
}
.footer {
height: 112px;
width: 100%;
position: fixed;
left: 0;
bottom: 0;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
& > div {
color: #333333;
}
& > div:first-child {
width: 50%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
& > div:last-child {
width: 50%;
height: 100%;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
background-color: #1365DD;
}
& > label {
display: flex;
align-items: center;
justify-content: center;
background-color: #1365DD;
}
}
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<div class="document-flow">
<ai-top-fixed>
<header class="pad">
<u-search placeholder="请输入公文名称" v-model="documentName" @clear="documentName='',getList()" @search="getList" clearabled :show-action="false" height="64"></u-search>
</header>
</ai-top-fixed>
<div class="list pad" v-if="list.length">
<div class="card" v-for="(item,index) in list" :key="index" @click="handleClick(item)">
<u-row>
<em v-if="item.redStatus==0"></em>
<span>{{item.documentName}}</span>
</u-row>
<u-gap height="16"></u-gap>
<u-row>
<label>公文类型</label>
<text style="color: #1365DD;">{{$dict.getLabel("officialDocumentName",item.documentType)}}</text>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<label>登记人</label>
<text>{{item.createUserName}}</text>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<label>登记日期</label>
<text>{{item.createTime}}</text>
</u-row>
<img :src=" $cdn + tag(item.readType)" alt="">
</div>
</div>
<AiEmpty v-else></AiEmpty>
<u-loadmore :status="status" v-if="list.length"/>
</div>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
import AiEmpty from "../../components/AiEmpty/AiEmpty";
export default {
name: "documentFlow",
components: {AiTopFixed,AiEmpty},
data() {
return {
documentName: "",
current: 1,
list: [],
status: "加载更多"
}
},
onLoad(){
this.$dict.load("officialDocumentName")
},
methods: {
tag(status){
return {
"0": 'common/1ps.png',
"1": 'common/2cy.png'
}[status]
},
getList() {
this.$http.post("/app/appofficialdocumentinfo/appList", null, {
params:{
documentName: this.documentName,
size: 10,
current: this.current
}
}).then(res => {
if (res && res.data) {
if (this.current > 1 && this.current > res.data.pages) {
this.status = "已经到底啦"
}
this.list = this.current > 1 ? [...this.list, ...res.data.records] : res.data.records
}
})
},
handleClick({id}) {
uni.navigateTo({
url: "/pages/documentFlow/components/detail?id=" + id
})
}
},
onShow(){
this.getList()
},
onReachBottom() {
this.current = this.current + 1;
this.getList()
},
}
</script>
<style lang="scss" scoped>
.document-flow {
min-height: 100%;
background: #F5F5F5;
::v-deep .content {
padding: 0;
}
header {
height: 112px;
background-color: #FFFFFF;
display: flex;
align-items: center;
}
.list {
margin: 32px 0;
.card {
background: #FFFFFF;
border-radius: 8px;
box-sizing: border-box;
padding: 32px;
position: relative;
margin-bottom: 32px;
.u-row{
flex-wrap: nowrap !important;
}
em {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #FF4466;
font-style: normal;
margin-right: 8px;
flex-shrink: 0;
}
span {
font-size: 32px;
font-weight: 600;
color: #333333;
}
label {
font-size: 30px;
color: #999999;
}
text {
font-size: 30px;
color: #343D65;
}
img {
width: 160px;
height: 160px;
position: absolute;
right: 0;
bottom: 0;
}
}
}
.pad {
box-sizing: border-box;
padding: 32px 32px 0 32px;
}
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<section class="areaSelector">
<ai-search-popup mode="bottom" ref="areaSelector">
<template #btn>
<div class="areaSelector">
<span v-for="area in fullArea" :key="area.id" v-text="area.name"
:class="{current:area.id==areaId}" @tap="index=area.id,getChildAreas(area.id)"/>
</div>
</template>
<div class="areaSelector">
<span v-for="area in fullArea" :key="area.id" v-text="area.name"
:class="{current:area.id==index}"
@click="index=area.id,getChildAreas(area.id)"/>
</div>
<div class="pendingItem" flex v-for="op in list" :key="op.id" @tap="handleSelect(op)">
<div class="fill" :class="{self:index==op.id}" v-html="op.name"/>
<u-icon name="arrow-right" color="#ddd"/>
</div>
</ai-search-popup>
</section>
</template>
<script>
import AiSearchPopup from "../../../components/AiSearchPopup";
import AiCell from "../../../components/AiCell";
import {mapState} from "vuex";
export default {
name: "areaSelector",
components: {AiCell, AiSearchPopup},
props: {
areaId: {default: ""}
},
computed: {
...mapState(['user']),
dataRange() {
let rules = [10, 8, 6, 3, 0], level = 2
rules.some((e, i) => {
let reg = new RegExp(`0{${e}}`, 'g')
if (reg.test(this.user.areaId)) {
return level = i
}
})
return level
}
},
data() {
return {
fullArea: [],
index: "",
list: []
}
},
watch: {
areaId(v) {
v && this.getFullArea()
}
},
methods: {
getFullArea() {
let {areaId} = this
return areaId && this.$http.post("/admin/area/getAllParentAreaId", null, {
params: {areaId}
}).then(res => {
if (res?.data) {
this.fullArea = res.data.reverse().slice(this.dataRange)
}
})
},
getChildAreas(id) {
id && this.$http.post("/admin/area/queryAreaByParentId", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.list = res.data
let self = this.fullArea.find(e => e.id == this.index)
this.list.unshift(self)
}
})
},
handleSelect(op) {
this.$emit('select', op)
this.$refs.areaSelector?.handleSelect()
}
},
created() {
this.index = this.areaId
this.getFullArea()
}
}
</script>
<style lang="scss" scoped>
.areaSelector {
::v-deep .AiSearchPopup {
.areaSelector {
display: flex;
align-items: center;
span {
cursor: pointer;
&:first-of-type:before {
content: "";
padding: 0;
}
&:before {
color: #333;
content: "/";
padding: 0 16px;
}
}
.current {
color: #3F8DF5;
}
}
.u-drawer-content {
position: fixed;
.areaSelector {
padding: 0 16px;
box-sizing: border-box;
border-bottom: 16px solid #f5f5f5;
span {
line-height: 100px;
}
}
}
.pendingItem {
margin-left: 32px;
padding-right: 32px;
height: 104px;
border-bottom: 1px solid #ddd;
.self {
font-weight: bold;
}
}
}
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<section class="makeCalls">
<div v-if="$slots.default" @tap="calls=true">
<slot/>
</div>
<div v-else flex class="column" @tap="calls=true">
<img :src="`${$cdn}guardianship/dh.png`"/>
<span v-html="label"/>
</div>
<u-popup v-model="calls" mode="bottom">
<div flex class="column option" v-for="item in list" @click="handleCall(item.guardianPhone)">
{{ [item.guardianName, item.guardianPhone].join(' ') }}
</div>
<div class="option" @tap="calls=false">取消</div>
</u-popup>
</section>
</template>
<script>
export default {
name: "makeCalls",
props: {
label: {default: "联系ta"},
list: {default: () => []}
},
data() {
return {
calls: false
}
},
methods: {
handleCall(phone) {
location.href = "tel:" + phone
},
}
}
</script>
<style lang="scss" scoped>
.makeCalls {
img {
width: 40px;
height: 40px;
}
::v-deep span {
margin-left: 0;
color: #999;
font-size: 20px;
p {
color: #3D94FB;
}
}
::v-deep .u-drawer {
text-align: center;
.uni-scroll-view-content{
max-height: 672px;
}
.option {
font-size: 32px;
cursor: pointer;
line-height: 112px;
border-bottom: 1px solid #D8DDE6;
&:first-of-type {
border-radius: 16px 16px 0 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<section class="openMap">
<div flex class="column" shrink @tap="handleOpenMap">
<img :src="`${$cdn}guardianship/seat.png`"/>
<span v-text="'地图/导航'"/>
</div>
</section>
</template>
<script>
export default {
name: "openMap",
props: {
data: {default: () => ({})}
},
methods: {
handleOpenMap() {
let {lng, lat, gpsDesc} = this.data
location.href = `https://uri.amap.com/marker?callnative=1&position=${[lng, lat].toString()}&name=${gpsDesc}`
}
}
}
</script>
<style lang="scss" scoped>
.openMap {
flex-shrink: 0;
img {
width: 40px;
height: 40px;
}
span {
margin-left: 0;
color: #999;
font-size: 20px;
}
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<section class="earlyWarning">
<ai-top-fixed>
<u-search v-model="search.name" placeholder="请输入姓名" :show-action="false"
search-icon-color="#ccc" placeholder-color="#999"
@change="page.current=1,getList()"/>
<div flex>
<ai-date class="fill" placeholder="日期选择" mode="range" @change="handleDateSearch"/>
<ai-select class="fill" dict="intelligentGuardianshipItem2" @data="handleTypeSearch">
<div>{{ $dict.getLabel('intelligentGuardianshipItem2', search.item) || '全部预警' }}</div>
<i class="iconfont iconfont-iconArrow_Down"/>
</ai-select>
</div>
</ai-top-fixed>
<div class="card" v-for="row in list" :key="row.id" @tap="handleShow(row)">
<div class="header" flex>
<img :src="top.cdn(typeIcons[row.item])"/>
<b v-text="row.desc"/>
</div>
<div class="wrapper">
<div class="start" flex>
<span v-text="`上报时间:`"/>
<div v-text="row.createTime"/>
</div>
<div class="start" flex>
<span v-text="`上报地点:`"/>
<div v-text="row.gpsDesc"/>
</div>
</div>
</div>
</section>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
import {mapState} from "vuex";
import AiDate from "../../components/AiDate";
import AiSelect from "../../components/AiSelect";
export default {
name: "earlyWarning",
components: {AiSelect, AiDate, AiTopFixed},
inject: ['top'],
computed: {
...mapState(['user']),
typeIcons() {
return {
0: "icon4",
1: "icon2",
2: "icon5",
3: "icon6",
4: "icon3",
5: "icon1",
}
}
},
data() {
return {
search: {name: "", createTimeRange: ",", item: ""},
areaId: "",
page: {current: 1, size: 10, total: 0},
list: []
}
},
methods: {
getList() {
let {areaId} = this
this.$http.post("/app/appintelligentguardianshipalarm/list", null, {
params: {areaId, ...this.search, ...this.page}
}).then(res => {
if (res?.data) {
let data = res.data.records.map(e => {
return {...e, desc: [e.name, this.$dict.getLabel('intelligentGuardianshipItem2', e.item)].join('的')}
})
if (this.page.current > 1) {
this.list = [...this.list, ...data]
} else this.list = data
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
handleDateSearch(v) {
let {startDate: start, endDate: end} = v
start = this.$dateFormat(start)
end = this.$dateFormat(end)
this.search.createTimeRange = [start, end || start].toString()
this.page.current = 1
this.getList()
},
handleTypeSearch(v) {
this.search.item = v?.[0]?.value
this.page.current = 1
this.getList()
},
handleShow(row) {
uni.navigateTo({url: `./warningDetail?id=${row.id}`})
}
},
created() {
this.areaId = JSON.parse(JSON.stringify(this.user.areaId))
this.getList()
}
}
</script>
<style lang="scss" scoped>
.earlyWarning {
padding-bottom: 130px;
background: #f5f5f5;
::v-deep .AiDate > div {
justify-content: center;
}
::v-deep .u-drawer-content {
padding-bottom: 100px;
}
::v-deep .display {
justify-content: center;
}
::v-deep .iconfont-iconArrow_Down {
margin-left: 4px;
font-size: 32px;
color: inherit;
}
::v-deep .card {
margin: 32px 32px 0;
background: #FFFFFF;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
border-radius: 8px;
.header {
padding: 0 32px;
height: 104px;
border-bottom: 2px solid #EFEFF4;
font-size: 36px;
img {
width: 64px;
height: 64px;
margin-right: 16px;
}
}
.wrapper {
color: #343D65;
font-size: 30px;
margin-bottom: 8px;
padding: 18px 32px;
span {
white-space: nowrap;
flex-shrink: 0;
color: #999;
margin-right: 20px;
}
}
}
}
</style>

View File

@@ -0,0 +1,461 @@
<template>
<section class="gsLocation">
<ai-map class="fill" :map.sync="amap" :lib.sync="mapLib"/>
<div class="searchZone">
<u-search v-model="search" placeholder="请输入姓名" shape="square" bg-color="#fff"
:show-action="false" @search="getList()"/>
<div class="searchResult" v-if="searchResult">
<div class="item" v-for="row in list" :key="row.id" @tap="handleSelect(row)">
<img :src="cdn(row.onlineStatus==1?'zxtx':'lxtx')"/>
<div flex class="column fill">
<b v-html="searchName(row.name)"/>
<div v-text="row.gpsDesc"/>
</div>
</div>
</div>
<u-popup v-model="popup" mode="bottom" :mask="false">
<div class="headerIcon" flex @touchstart="handleTouchStart" @touchmove="handleTouchmoveClose"/>
<div class="selectedInfo">
<div class="header" flex>
<img :src="`${$cdn}guardianship/tx.png`" @tap="handleShowDetail(selected)"/>
<b v-text="selected.name" @tap="handleShowDetail(selected)"/>
<div v-if="selected.abnormalStatus==1" class="abnormal" @tap="handleShowDetail(selected)">异常</div>
<u-icon name="arrow-right" color="#ddd" class="fill" @tap="handleShowDetail(selected)"/>
<make-calls :list="phoneList"/>
</div>
<div flex class="spb wrap">
<div class="detail" v-for="(op,i) in quotas" :key="i" flex>
<img :src="op.icon"/>
<div class="fill" v-text="op.label"/>
<div :class="{abnormal:op.abnormal}" v-text="op.value"/>
</div>
</div>
</div>
<div class="navigation">
<div class="content" flex>
<div flex class="spb wrap">
<div class="fill" v-text="selected.gpsDesc"/>
<span>最后更新{{ selected.lastUpdateTime }}</span>
<div class="battery" flex>
<img :src="batteryIcon"/>
<div v-text="`剩余${selected.electricQuantity}%`"/>
</div>
</div>
<open-map :data="selected"/>
</div>
</div>
</u-popup>
</div>
</section>
</template>
<script>
import AiSearchPopup from "../../components/AiSearchPopup";
import {mapState} from "vuex";
import UPopup from "../../uview/components/u-popup/u-popup";
import MakeCalls from "./component/makeCalls";
import OpenMap from "./component/openMap";
import AiMap from "../../components/AiMap";
export default {
name: "gsLocation",
components: {AiMap, OpenMap, MakeCalls, UPopup, AiSearchPopup},
computed: {
...mapState(['user']),
markers() {
return this.list.filter(e => e.lng).map(e => {
let abnormal = 'offline'
if (e.onlineStatus == 1) {
switch (e.abnormalStatus) {
case '1':
abnormal = 'warning';
break;
case '2':
abnormal = 'abnormal';
break;
default:
abnormal = ''
}
}
return new this.mapLib.Marker({
position: new this.mapLib.LngLat(e.lng, e.lat),
anchor: 'bottom-center',
content: `<div class="marker ${abnormal}">${e.name}</div>`,
extData: e,
topWhenClick: true
}).on('click', () => {
this.handleSelect(e)
})
})
},
quotas() {
let quota = [
{key: "0", icon: "1"},
{key: "1", icon: "2"},
{key: "2", icon: "3"},
{key: "3", icon: "4"},
]
return quota.map(e => {
let item = this.detail.find(d => d.item == e.key)
let label = this.$dict.getLabel('intelligentGuardianshipItem', e.key)
return {
label, icon: this.cdn(e.icon),
value: item?.itemValue || "-",
abnormal: item?.abnormalStatus == 1
}
})
},
batteryIcon() {
return this.cdn(this.selected.electricQuantity == 100 ? 'dcm' : 'dcq')
},
phoneList() {
let {name: guardianName, phone: guardianPhone} = this.selected
return [{guardianName, guardianPhone}, ...(this.selected.guardians || [])]
},
},
data() {
return {
mapLib: null,
amap: null,
search: "",
selected: {},
list: [],
popup: false,//被监护人信息弹窗
detail: [],
moveDistance: 0,
searchResult: false //搜索下拉弹窗
}
},
watch: {
amap(v) {
v && this.getList().then(() => {
this.amap?.add(this.markers)
this.getMapArea()
})
}
},
methods: {
cdn(icon) {
return `${this.$cdn}guardianship/${icon}.png`
},
getMapArea() {
if (this.mapLib) {
new this.mapLib.DistrictSearch({
subdistrict: 0, //获取边界不需要返回下级行政区
extensions: 'all', //返回行政区边界坐标组等具体信息
level: 'district' //查询行政级别为 市
}).search(this.user.areaId.substring(0, 6), (status, result) => {
let bounds = result?.districtList?.[0]?.boundaries;
let polygons = []
bounds?.forEach(path => polygons.push(new this.mapLib.Polygon({
strokeWeight: 1,
path,
strokeStyle: 'dashed',
fillOpacity: 0.1,
fillColor: '#80d8ff',
strokeColor: '#0091ea'
})))
this.amap.add(polygons)
this.amap.setFitView();//视口自适应
})
}
},
getList() {
this.searchResult = !!this.search
return this.$http.post("/app/appintelligentguardianshipdevice/list", null, {
params: {name: this.search, size: 999, areaId: this.user.areaId}
}).then(res => {
if (res?.data) {
this.list = res.data.records
}
})
},
getDetail(deviceId) {
this.$http.post("/app/appintelligentguardianshipdevice/queryMonitorList", null, {
params: {type: 1, deviceId}
}).then(res => {
if (res?.data) {
this.detail = res.data.records
}
})
this.$http.post("/app/appintelligentguardianshipdevice/queryDetailById", null, {
params: {id: deviceId}
}).then(res => {
if (res?.data) {
this.selected = {...this.selected, ...res.data}
}
})
},
searchName(name) {
return name?.replace(this.search, `<span>${this.search}</span>`)
},
handleSelect(e) {
this.amap.setCenter(new this.mapLib.LngLat(e.lng, e.lat))
this.selected = e
this.popup = true
this.searchResult = false
this.getDetail(e.id)
},
handleShowDetail(user) {
uni.navigateTo({url: `./userDetail?id=${user.id}`})
},
handleTouchmoveClose(e) {
if (e.touches?.[0]?.clientY > this.moveDistance + 10) {
this.popup = false
}
},
handleTouchStart(e) {
this.moveDistance = e.touches?.[0]?.clientY
}
}
}
</script>
<style lang="scss" scoped>
.gsLocation {
height: 100%;
a {
color: inherit;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
height: 112px;
border-top: 1px solid #d8dde6;
&:first-of-type {
border-top: none;
}
}
.headerIcon {
width: 100%;
height: 60px;
justify-content: center;
&:before {
content: " ";
display: block;
width: 64px;
height: 10px;
background: #CCCCCC;
border-radius: 5px;
}
}
::v-deep .makeCalls {
.option:last-of-type {
margin-bottom: 120px;
}
}
::v-deep .selectedInfo {
width: 100%;
border-radius: 20px 20px 0 0;
padding: 0 32px;
background: #fff;
overflow: hidden;
box-sizing: border-box;
.header {
margin-bottom: 40px;
font-size: 40px;
& > img {
width: 82px;
height: 82px;
margin-right: 16px;
}
.abnormal {
color: #FF4466;
font-size: 24px;
padding: 12px;
background: rgba(#EC4461, .1);
border-radius: 8px;
margin: 0 16px;
}
span {
margin-left: 0;
color: #999;
font-size: 20px;
}
}
.detail {
width: 318px;
height: 84px;
background: #F4F5F6;
border-radius: 8px;
padding: 0 24px;
box-sizing: border-box;
margin-bottom: 36px;
img {
margin-right: 16px;
width: 56px;
height: 56px;
}
.abnormal {
color: #FF4466;
}
}
}
.navigation {
padding-bottom: 100px;
.content {
padding: 32px 40px;
font-size: 26px;
& > .spb {
margin-right: 40px;
}
.fill {
min-width: 100%;
flex-shrink: 0;
margin-bottom: 10px;
}
span {
margin-left: 0;
color: #999;
font-size: 22px;
}
.battery > img {
margin-right: 14px;
}
}
&:before {
content: " ";
display: block;
width: 100%;
height: 8px;
background: #F4F5F6;
}
}
::v-deep .searchZone {
position: absolute;
top: 0;
z-index: 2;
width: 100%;
background: transparent;
padding: 24px 16px;
box-sizing: border-box;
.u-search {
box-shadow: 0 4px 8px 0 rgba(192, 185, 185, 0.5);
}
.searchResult {
margin-top: 16px;
padding: 0 28px;
background: #fff;
.item {
font-size: 24px;
display: flex;
line-height: 36px;
padding: 24px 0;
border-bottom: 3px solid #DEDFE1;
&:last-of-type {
border-bottom: none;
}
img {
width: 36px;
height: 36px;
margin-right: 16px;
}
& > .fill {
align-items: unset;
b > span {
color: #1365DD;
}
& > div {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
::v-deep .marker {
color: #fff;
font-size: 30px;
display: flex;
justify-content: center;
align-items: center;
padding: 0 32px;
height: 56px;
white-space: nowrap;
background: #5088FF;
border-color: #5088FF;
border-radius: 52px;
position: relative;
&:after {
position: absolute;
display: block;
content: " ";
bottom: -12px;
left: 50%;
transform: translateX(-50%);
border: 12px solid transparent;
border-bottom: none;
height: 0;
width: 0;
border-top-color: inherit;
}
&.offline {
background: #C4CAD4;
border-color: #C4CAD4;
}
&.warning {
background: #FFAA44;
border-color: #FFAA44;
}
&.abnormal {
background: #F46159;
border-color: #F46159;
&:before {
position: absolute;
z-index: -1;
bottom: -40px;
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #F46159;
transform: translate(-50%, -50%);
animation: mapWarn 1s ease-out 0s infinite;
content: " ";
}
}
}
.AiMap {
width: 100%;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<section class="guardianship">
<component ref="currentTab" :is="currentTab.comp"/>
<ai-tabbar :active.sync="active" :list="bottomBar"/>
</section>
</template>
<script>
import AiLoading from "../../components/AiLoading";
import GsLocation from "./gsLocation";
import AiTabbar from "../../components/AiTabbar";
import WardList from "./wardList";
import EarlyWarning from "./earlyWarning";
export default {
name: "guardianship",
components: {AiTabbar, AiLoading},
provide() {
return {
top: this
}
},
computed: {
bottomBar() {
return [
{text: "定位", iconPath: "bardwn", selectedIconPath: "bardwh", comp: GsLocation},
{text: "人员", iconPath: "barryn", selectedIconPath: "barryh", comp: WardList},
{text: "预警", iconPath: "baryjn", selectedIconPath: "baryjh", comp: EarlyWarning},
].map(e => ({
...e,
iconPath: this.cdn(e.iconPath),
selectedIconPath: this.cdn(e.selectedIconPath)
}))
},
currentTab() {
return this.bottomBar?.[this.active] || {}
}
},
methods: {
cdn(icon) {
return `${this.$cdn}guardianship/${icon}.png`
},
},
data() {
return {
active: 0
}
},
created() {
this.$dict.load("intelligentGuardianshipItem", 'intelligentGuardianshipItem2', 'intelligentGuardianshipAbnormalStatus')
},
onReachBottom() {
if (typeof this.$refs?.currentTab?.reachBottom == 'function') this.$refs?.currentTab.reachBottom()
}
}
</script>
<style lang="scss" scoped>
.guardianship {
position: absolute;
width: 100%;
top: 0;
bottom: 0;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<section class="historyList">
<div flex>
<b class="header" v-text="itemLabel"/>
</div>
<div v-for="row in list" :key="row.id" flex class="spb row">
<div :class="{abnormal:row.abnormalStatus==1}" v-text="row.itemValue"/>
<span v-text="row.sampleTime"/>
</div>
<ai-back/>
</section>
</template>
<script>
import AiBack from "../../components/AiBack";
export default {
name: "historyList",
components: {AiBack},
computed: {
itemLabel() {
return '历史'+this.$dict.getLabel('intelligentGuardianshipItem', this.$route.query.type)+`(${this.$dict.getLabel('intelligentGuardianshipItemUnit',this.$route.query.type)})`
}
},
data() {
return {
list: []
}
},
methods: {
getHistory() {
let {type: item, id: deviceId} = this.$route.query
this.$http.post("/app/appintelligentguardianshipdevice/queryMonitorList", null, {
params: {deviceId, size: 999, item}
}).then(res => {
if (res?.data) {
this.list = res.data.records
}
})
}
},
created() {
this.$dict.load("intelligentGuardianshipItem",'intelligentGuardianshipItemUnit')
this.getHistory()
}
}
</script>
<style lang="scss" scoped>
.historyList {
font-size: 30px;
& > div {
padding: 0 32px;
border-bottom: 1px solid #ddd;
height: 96px;
background: #FFFFFF;
}
.header {
font-size: 32px;
}
.row {
color: #5AAD6A;
.abnormal {
color: #CD413A;
}
& > span {
color: #999;
}
}
}
</style>

View File

@@ -0,0 +1,279 @@
<template>
<section class="userDetail">
<div class="selectedInfo">
<div class="header" flex>
<img :src="`${$cdn}guardianship/tx.png`"/>
<b v-text="detail.name"/>
<div v-if="detail.abnormalStatus==1" class="abnormal">异常</div>
<div class="fill"/>
<make-calls :list="phoneList" :label="`<p>拨打电话</p>`"/>
</div>
<div class="content">
<ai-cell label="所属地区">{{ detail.areaName }}</ai-cell>
<ai-cell label="联系电话">{{ detail.phone }}</ai-cell>
<ai-cell label="性别">{{ $dict.getLabel('sex', detail.sex) }}</ai-cell>
<ai-cell label="年龄">{{ $calcAge(detail.idNumber) }}</ai-cell>
</div>
</div>
<div class="card">
<div flex class="spb header">
<b v-text="`设备状况`"/>
<span class="onlineStatus" v-html="detail.onlineStatus==1?'设备在线':'<p>设备离线</p>'"/>
</div>
<div flex class="spb wrap quotas">
<div class="quota" v-for="(op,i) in quotas" :key="i" flex @tap="handleShowHistory(op)">
<img :src="op.icon"/>
<div class="fill" v-text="op.label"/>
<div :class="{abnormal:op.abnormal}" v-text="op.value"/>
<u-icon name="arrow-right" color="#ddd"/>
</div>
</div>
<div class="navigation">
<div class="content spb" flex>
<div flex class="spb wrap">
<div class="fill" v-text="detail.gpsDesc"/>
<span>最后更新{{ detail.lastUpdateTime }}</span>
<div class="battery" flex>
<img :src="batteryIcon"/>
<div v-text="`剩余${detail.electricQuantity||0}%`"/>
</div>
</div>
<open-map :data="detail"/>
</div>
</div>
</div>
<div class="card">
<div flex class="spb header">
<b v-text="`监护人信息`"/>
</div>
<div flex class="spb guardian" v-for="row in detail.guardians" :key="row.id">
<span v-text="row.guardianName"/>
<div v-text="row.guardianPhone"/>
</div>
</div>
<ai-back/>
</section>
</template>
<script>
import MakeCalls from "./component/makeCalls";
import OpenMap from "./component/openMap";
import AiCell from "../../components/AiCell";
import AiBack from "../../components/AiBack";
export default {
name: "userDetail",
components: {AiBack, AiCell, OpenMap, MakeCalls},
computed: {
batteryIcon() {
return this.cdn(this.detail.electricQuantity == 100 ? 'dcm' : 'dcq')
},
quotas() {
let quota = [
{key: "0", icon: "1"},
{key: "1", icon: "2"},
{key: "2", icon: "3"},
{key: "3", icon: "4"},
]
return quota.map(e => {
let item = this.detail.quota?.find(d => d.item == e.key)
let label = this.$dict.getLabel('intelligentGuardianshipItem', e.key)
return {
label, icon: this.cdn(e.icon), type: e.key,
value: item?.itemValue || "-",
abnormal: item?.abnormalStatus == 1
}
})
},
phoneList() {
let {name: guardianName, phone: guardianPhone} = this.detail
return [{guardianName, guardianPhone}, ...this.detail.guardians]
}
},
data() {
return {
detail: {
guardians: []
}
}
},
methods: {
cdn(icon) {
return `${this.$cdn}guardianship/${icon}.png`
},
getDetail(deviceId) {
this.$http.post("/app/appintelligentguardianshipdevice/queryMonitorList", null, {
params: {type: 1, deviceId}
}).then(res => {
if (res?.data) {
this.$set(this.detail, 'quota', res.data.records)
}
})
this.$http.post("/app/appintelligentguardianshipdevice/queryDetailById", null, {
params: {id: deviceId}
}).then(res => {
if (res?.data) {
this.detail = {...this.detail, ...res.data}
}
})
},
handleShowHistory(item) {
uni.navigateTo({url: `./historyList?type=${item.type}&id=${this.$route.query.id}`})
}
},
created() {
this.$dict.load("intelligentGuardianshipItem", 'sex')
this.getDetail(this.$route.query.id)
}
}
</script>
<style lang="scss" scoped>
.userDetail {
padding-bottom: 60px;
& > * {
margin-bottom: 16px;
}
.card {
background: #fff;
font-size: 32px;
box-shadow: 0 1px 1px 0 rgba(221, 221, 221, 1);
.header {
height: 96px;
background: #FFFFFF;
border-bottom: 1px solid #ddd;
}
& > .spb {
padding: 0 32px;
}
}
::v-deep .selectedInfo {
width: 100%;
background: #fff;
overflow: hidden;
box-sizing: border-box;
.header {
height: 136px;
font-size: 40px;
padding: 0 32px;
border-bottom: 1px solid #D8DDE6;
& > img {
width: 82px;
height: 82px;
margin-right: 16px;
}
.abnormal {
color: #FF4466;
font-size: 24px;
padding: 12px;
background: rgba(#EC4461, .1);
border-radius: 8px;
margin-left: 16px;
}
span {
margin-left: 0;
color: #999;
font-size: 20px;
}
}
& > .content {
padding: 32px;
font-size: 30px;
::v-deep .AiCell {
padding: 0;
min-height: 58px;
}
}
}
.quotas {
margin-top: 24px;
.quota {
cursor: pointer;
width: 318px;
height: 84px;
background: #F4F5F6;
border-radius: 8px;
padding: 0 8px 0 24px;
box-sizing: border-box;
margin-bottom: 36px;
font-size: 28px;
img {
margin-right: 16px;
width: 56px;
height: 56px;
}
.abnormal {
color: #FF4466;
}
}
}
.navigation {
.content {
padding: 10px 40px 32px;
font-size: 26px;
& > .spb {
margin-right: 40px;
}
.fill {
min-width: 100%;
flex-shrink: 0;
margin-bottom: 10px;
}
span {
margin-left: 0;
color: #999;
font-size: 22px;
}
.battery > img {
margin-left: 32px;
margin-right: 14px;
}
}
}
::v-deep .onlineStatus {
font-size: 30px;
color: #5AAD6A;
p {
color: #F5A319;
}
}
.guardian {
height: 96px;
border-bottom: 1px solid #ddd;
font-size: 28px;
color: #222;
&:last-of-type {
border-bottom: none;
}
span {
color: #666666;
}
}
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<section class="wardList">
<ai-top-fixed>
<u-search v-model="search" placeholder="请输入姓名" :show-action="false"
search-icon-color="#ccc" placeholder-color="#999"
@change="page.current=1,getUser()"/>
<area-selector :areaId="areaId" @select="handleSelectArea"/>
</ai-top-fixed>
<div class="userList">
<div v-for="row in list" :key="row.id" flex class="row" @tap="handleShowDetail(row)">
<img :src="top.cdn(row.onlineStatus==1?'zxtx':'lxtx')"/>
<b class="fill" v-text="row.name"/>
<div class="status" :style="{color:$dict.getColor('intelligentGuardianshipAbnormalStatus',row.abnormalStatus)}"
v-text="$dict.getLabel('intelligentGuardianshipAbnormalStatus',row.abnormalStatus)"/>
<u-icon name="arrow-right" color="#ddd"/>
</div>
</div>
</section>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
import {mapState} from "vuex";
import AreaSelector from "./component/areaSelector";
export default {
name: "wardList",
components: {AreaSelector, AiTopFixed},
computed: {
...mapState(['user']),
},
inject: ['top'],
data() {
return {
search: "",
areaId: "",
page: {current: 1, size: 20, total: 0},
list: []
}
},
methods: {
getUser() {
let {areaId, search: name} = this
this.$http.post("/app/appintelligentguardianshipdevice/list", null, {
params: {areaId, name, ...this.page}
}).then(res => {
if (res?.data) {
let data = res.data.records.reverse()
if (this.page.current > 1) {
this.list = [...this.list, ...data]
} else this.list = data
this.page.total = res.data.total
}
})
},
handleSelectArea({id}) {
this.areaId = id
this.getUser()
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getUser()
}
},
handleShowDetail(user) {
uni.navigateTo({url: `./userDetail?id=${user.id}`})
}
},
created() {
this.areaId = JSON.parse(JSON.stringify(this.user.areaId))
this.getUser()
}
}
</script>
<style lang="scss" scoped>
.wardList {
background: #f5f5f5;
padding-bottom: 130px;
font-size: 30px;
::v-deep .userList {
margin-top: 16px;
background: #fff;
.row {
font-size: 36px;
margin-left: 32px;
padding-right: 32px;
height: 104px;
border-bottom: 1px solid #ddd;
img {
width: 72px;
height: 72px;
margin-right: 38px;
}
.status {
margin-right: 12px;
}
}
}
}
</style>

View File

@@ -0,0 +1,185 @@
<template>
<section class="warningDetail">
<div flex class="header">
<b v-text="detail.name"/>
<div>
{{ $dict.getLabel('intelligentGuardianshipItem2', detail.item) }}
{{ detail.itemValue }}
</div>
</div>
<ai-map class="fill" :map.sync="amap" :lib.sync="mapLib"/>
<div class="navigation">
<div class="content spb" flex>
<div flex class="spb wrap">
<div class="fill" v-text="detail.gpsDesc"/>
<span>最后更新{{ detail.createTime }}</span>
</div>
<open-map :data="detail"/>
</div>
</div>
<make-calls :list="phoneList">
<div class="bottomBtn">拨打电话</div>
</make-calls>
<ai-back/>
</section>
</template>
<script>
import OpenMap from "./component/openMap";
import MakeCalls from "./component/makeCalls";
import AiBack from "../../components/AiBack";
import AiMap from "../../components/AiMap";
export default {
name: "warningDetail",
components: {AiMap, AiBack, MakeCalls, OpenMap},
computed: {
phoneList() {
let {name: guardianName, phone: guardianPhone} = this.detail
return [{guardianName, guardianPhone}, ...this.detail.guardians]
}
},
data() {
return {
detail: {guardians: []},
mapLib: null,
amap: null,
}
},
methods: {
getDetail(id) {
return this.$http.post("/app/appintelligentguardianshipalarm/queryDetailById", null, {
params: {id},
withoutToken: true
}).then(res => {
if (res?.data) {
this.detail = res.data
}
})
},
initMap() {
if (this.mapLib) {
let pos = new this.mapLib.LngLat(this.detail.lng, this.detail.lat)
this.amap.add(new this.mapLib.Marker({
position: pos,
anchor: 'bottom-center',
content: `<div class="marker">${this.detail.name}</div>`,
}))
this.amap.setFitView()
}
}
},
watch: {
mapLib(v) {
this.detail.id && v && this.initMap()
}
},
created() {
this.$dict.load("intelligentGuardianshipItem",'intelligentGuardianshipItem2', 'sex')
},
mounted() {
this.getDetail(this.$route.query.id).then(() => this.initMap())
}
}
</script>
<style lang="scss" scoped>
.warningDetail {
padding: 48px 48px 112px;
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: absolute;
.header {
font-size: 40px;
font-weight: bold;
color: #333333;
justify-content: center;
margin-bottom: 48px;
& > div {
color: #EC4461;
}
}
.navigation {
.content {
padding: 10px 0 32px;
font-size: 28px;
color: #555;
& > .spb {
margin-right: 40px;
}
.fill {
min-width: 100%;
flex-shrink: 0;
margin-bottom: 10px;
}
span {
margin-left: 0;
color: #999;
font-size: 22px;
}
}
}
.bottomBtn {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
background: #1365DD;
line-height: 112px;
}
.AiMap {
margin-bottom: 72px;
}
::v-deep .marker {
border-radius: 52px;
background: #F46159;
color: #fff;
font-size: 22px;
padding: 4px 16px;
white-space: nowrap;
position: relative;
&:before {
position: absolute;
left: calc(50% - 30px);
bottom: -34px;
z-index: -1;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #F46159;
animation: mapWarn 1s ease-out 0s infinite;
content: " ";
}
&:after {
position: absolute;
display: block;
content: " ";
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border: 8px solid transparent;
border-bottom: none;
border-top-color: #F46159;
height: 0;
width: 0;
}
}
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<div class="interviewDetail">
<template v-if="isEdit">
<u-form ref="interviewForm" label-position="top" :rules="rules" :model="form">
<u-form-item label="调查走访事项" prop="title" required>
<u-input v-model="form.title" placeholder="请输入最多30字" maxlength="30"/>
</u-form-item>
<u-form-item label="调查走访内容" prop="content">
<ai-textarea v-model="form.content" placeholder="请输入最多500字" :maxlength="500"/>
</u-form-item>
<u-form-item label="图片最多9张">
<ai-uploader multiple :limit="9" :def.sync="form.fileList" action="/admin/file/add2"/>
</u-form-item>
</u-form>
<div bottom>
<u-button type="primary" @tap="submitForm">保存</u-button>
</div>
</template>
<template v-else>
<div class="headerPane">
<b>{{ form.title }}</b>
<div>记录时间{{ form.createTime }}</div>
</div>
<div class="contentPane">
<div v-html="form.content"/>
<div flex class="wrap">
<ai-image v-for="(op,i) in form.fileList" :src="op.accessUrl" preview :key="i"/>
</div>
</div>
</template>
<ai-back/>
</div>
</template>
<script>
import UButton from "../../uview/components/u-button/u-button";
import AiUploader from "../../components/AiUploader";
import UInput from "../../uview/components/u-input/u-input";
import AiImage from "../../components/AiImage";
import AiTextarea from "../../components/AiTextarea";
import UFormItem from "../../uview/components/u-form-item/u-form-item";
import AiBack from "../../components/AiBack";
export default {
name: 'interviewDetail',
components: {AiBack, UFormItem, AiTextarea, AiImage, UInput, AiUploader, UButton},
computed: {
isEdit() {
let flag = this.$route.query?.detail != 1
!flag && uni.setNavigationBarTitle({title: "走访详情"})
return flag
},
rules() {
return {
title: [{required: true, message: '请输入 调查走访事项'}],
// content: [{required: true, message: '请输入 调查走访内容'}],
}
}
},
data() {
return {
form: {
fileList: []
}
}
},
created() {
this.searchDetail();
},
methods: {
submitForm() {
this.$refs.interviewForm?.validate(v => {
if (v) {
this.$http.post(`/app/appinterview/add-xcx`, {
...this.form
}).then(res => {
if (res?.code == 0) {
this.$u.toast("提交成功!")
uni.navigateBack()
}
})
}
})
},
searchDetail() {
let {id} = this.$route.query
id && this.$http.post(`/app/appinterview/queryDetailById`, null, {
params: {id}
}).then(res => {
if (res?.data) {
this.form = {...res.data};
}
})
},
}
}
</script>
<style lang="scss" scoped>
.interviewDetail {
background: #F3F6F9;
min-height: 100%;
.u-form {
width: 100%;
height: 100%;
overflow-y: auto;
background-color: #f3f6f9;
position: relative;
padding: 0 0 188px;
box-sizing: border-box;
font-size: 30px;
::v-deep textarea {
width: 100%;
}
::v-deep .u-form-item {
margin-bottom: 16px;
.u-form-item--left__content__label {
font-weight: 400;
}
div[flex] {
width: 100%;
}
}
}
div[bottom] {
z-index: 99;
padding: 0;
height: 112px;
.u-btn {
height: 100%;
border-radius: 0;
}
}
::v-deep .headerPane {
width: 100%;
background: #3975C6;
color: #fff;
padding: 24px 32px 32px;
box-sizing: border-box;
font-size: 28px;
b {
display: block;
font-size: 40px;
line-height: 64px;
letter-spacing: 2px;
margin-bottom: 16px;
}
}
::v-deep .contentPane {
padding: 32px;
width: 100%;
box-sizing: border-box;
font-size: 32px;
font-weight: 400;
color: #666;
line-height: 56px;
.wrap {
margin-top: 32px;
}
.AiImage {
width: 31%;
margin-bottom: 16px;
margin-right: 16px;
image {
width: 100%;
height: 218px;
}
}
}
}
</style>

View File

@@ -0,0 +1,224 @@
<template>
<div class="interview">
<ai-top-fixed>
<div flex>
<ai-date placeholder="日期选择" mode="range" @change="handleDateSearch"/>
<u-search placeholder="请输入标题" :show-action="false" v-model="search.title" @search="current=1,getList()"/>
</div>
</ai-top-fixed>
<template v-if="list.length>0">
<ai-card v-for="(e,index) in list" :key="index" @click.native="goDetail(e.id,1)">
<template #custom>
<div flex>
<b class="fill">{{ e.title }}</b>
</div>
<div flex v-if="!!e.fileList" class="wrap">
<ai-image v-for="(op,i) in e.fileList.slice(0,3)" :src="op.accessUrl" preview :key="i"/>
</div>
<div class="bottom">{{ e.createTime }}</div>
</template>
<template #menu>
<div class="menu" @tap.stop="goDetail(e.id)">编辑</div>
<div class="menu" @tap.stop="handleDelete(e.id)">删除</div>
</template>
</ai-card>
<u-loadmore :status="loadmore" color="#999" font-size="24"
margin-top="32" margin-bottom="80"/>
</template>
<div class="no-message" v-else>
<image src="https://cdn.cunwuyun.cn/wxAdmin/img/message.png"/>
<p>您还未添加过入户调查走访<br>点击<b>新增按钮</b>试试吧~</p>
</div>
<ai-fixed-btn>
<div class="addBtn iconfont iconfont-iconfangda" @tap="gotoAdd()"/>
</ai-fixed-btn>
</div>
</template>
<script>
import AiSelect from "../../components/AiSelect";
import AiTopFixed from "../../components/AiTopFixed";
import AiCard from "../../components/AiCard";
import AiImage from "../../components/AiImage";
import AiDate from "../../components/AiDate";
import AiFixedBtn from "../../components/AiFixedBtn";
export default {
name: "interview",
components: {AiFixedBtn, AiDate, AiImage, AiCard, AiTopFixed, AiSelect},
data() {
return {
search: {title: ""},
list: [],
current: 1,
pages: 0
}
},
computed: {
loadmore() {
return this.pages <= this.current ? 'loading ' : 'nomore'
}
},
onShow() {
this.current = 1;
this.getList()
},
onReachBottom() {
this.current++;
this.getList()
},
methods: {
getList() {
this.$http.post('/app/appinterview/list-xcx', null, {
params: {
current: this.current,
size: 10,
...this.search
}
}).then(res => {
if (res?.data) {
this.list = this.current > 1 ? [...this.list, ...res.data.records] : res.data.records
this.pages = res.data.pages
}
})
},
goDetail(id, readonly) {
let url = `./detail?id=${id}`
readonly && (url += "&detail=1")
uni.navigateTo({url})
},
gotoAdd() {
uni.navigateTo({url: `./detail`})
},
handleDelete(ids) {
this.$confirm("是否要删除该调查走访").then(() => {
this.$http.post("/app/appinterview/delete", null, {
params: {ids}
}).then(res => {
if (res?.code == 0) {
this.$u.toast("删除成功!")
this.getList()
}
})
})
},
handleDateSearch(v) {
this.search.startTime = v.startDate
this.search.endTime = v.endDate || v.startDate
this.current = 1
this.getList()
}
}
}
</script>
<style lang="scss" scoped>
.interview {
width: 100%;
min-height: 100%;
box-sizing: border-box;
position: absolute;
background: #fff;
.no-message {
margin-top: 140px;
text-align: center;
color: #888;
font-size: 30px;
b {
font-size: 32px;
color: $uni-color-primary;
padding: 0 8px;
}
image {
width: 320px;
height: 240px;
}
}
::v-deep .AiCard {
width: 100%;
min-height: 160px;
background: #FFFFFF;
padding: 32px 32px 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
b {
display: block;
width: 100%;
font-size: 30px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #333;
margin-right: 60px;
margin-bottom: 20px;
}
.iconfont-iconMore {
color: #666;
position: absolute;
right: 32px;
top: 32px;
}
.bottom {
font-size: 24px;
color: #999999;
padding: 24px 0;
border-bottom: 1px solid rgba(221, 221, 221, .4);
}
.AiImage {
width: 30%;
margin-bottom: 8px;
margin-right: 8px;
image {
width: 100%;
height: 218px;
}
}
}
.addBtn {
width: 96px;
height: 96px;
flex-shrink: 0;
background: $uni-color-primary;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
font-size: 48px;
color: #fff;
border-radius: 50%;
justify-content: center;
align-items: center;
display: flex;
}
.menu {
text-align: center;
line-height: 80px;
width: 192px;
height: 80px;
font-size: 28px;
font-weight: 400;
color: #333333;
}
::v-deep .u-search {
margin-bottom: 0 !important;
padding-left: 146px;
box-shadow: none;
.u-content {
padding-left: 50px;
box-sizing: border-box;
}
}
}
</style>

174
src/pages/loading.vue Normal file
View File

@@ -0,0 +1,174 @@
<template>
<section class="loading">
<!-- <div class="iconfont iconfont-iconWeChat"/>-->
<!-- <div class="iconfont iconfont-iconjuminxinxi"/>-->
<!-- <div class="iconfont iconfont-iconLogo"/>-->
<ai-result v-if="result.tips" v-bind="result"/>
<template v-if="isDev">
<input v-if="!!$route.query.code" class="codeText" :value="$route.query.code"/>
<div class="codeBtn" @click="devGetCode">获取code</div>
<div flex class="appsPane wrap">
<b v-for="app in apps" :key="app.key" @tap="gotoApp(app.key)">{{ app.name }}</b>
</div>
</template>
</section>
</template>
<script>
import {mapActions, mapState} from 'vuex'
import AiResult from "../components/AiResult";
import UTag from "../uview/components/u-tag/u-tag";
export default {
name: 'loading',
components: {UTag, AiResult},
inject: ['root'],
computed: {
...mapState(['token', 'apps', 'openUser', 'user']),
currentApp() {
return this.apps.find(e => e.key == this.$route.query.app) || {}
},
isDev() {
return this.$route.hash == "#dev"
}
},
data() {
return {
result: {}
}
},
methods: {
...mapActions(['getToken', 'getAccount', 'agentSign', 'getUserInfo', 'getCode', 'closeAgent']),
initAccess() {
if (this.$route.hash == "#error" || this.isDev) {
return Promise.resolve()
} else if (this.$route.hash == "#form") {
if (this.openUser?.openId || !!this.$route.query.preview) {
this.openForm()
} else if (this.$route.query?.code) {
this.getToken(this.$route.query?.code)
.then(() => this.getUserInfo())
.then(() => this.openForm())
} else this.getCode(location.href)
} else if (this.token) {//获取账号信息
return this.getAccount()
} else if (this.$route.query?.code) {//获取token
return this.getToken(this.$route.query?.code)
} else {//获取应用配置
this.getCode(location.href)
}
},
openForm() {
this.redirectTo("/askForm/askForm")
},
redirectTo(path) {
let {query, hash} = this.$route
delete query.app
uni.redirectTo({
url: `/pages${path}`, success: () => {
this.$router.push({query, hash})
}
})
},
gotoApp(app) {
uni.reLaunch({url: '/pages/loading?app=' + app})
},
devGetCode() {
this.getCode(location.origin + '/pages/loading#dev')
}
},
created() {
uni.showLoading({
title: "加载中"
})
this.initAccess()?.then(() => {
uni.hideLoading()
if (this.token) {
if (this.currentApp.name) {
this.redirectTo(this.currentApp.path)
} else if (this.$route.query.url) {
this.redirectTo(this.$route.query.url)
} else {
this.result = {
status: "error",
tips: "应用加载失败",
btn: "重新加载",
btnTap() {
location.href = location.href?.replace("#error", '')
}
}
}
} else if (this.isDev) {
this.result = {
tips: "欢迎进入开发应用",
}
} else {
this.result = {
status: "error",
tips: "应用加载失败",
btn: "重新加载",
btnTap() {
location.href = location.href?.replace("#error", '')
}
}
}
})?.catch(() => {
uni.navigateTo({url: './login'})
})
}
}
</script>
<style lang="scss" scoped>
.loading {
height: 100%;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
& > span {
font-size: 36px;
}
.codeText {
font-size: 24px;
margin-top: 16px;
padding: 4px 0;
width: 100%;
text-align: center;
background: #ccc;
}
.codeBtn {
width: 180px;
text-align: center;
cursor: pointer;
font-size: 24px;
background: $uni-color-warning;
color: #fff;
padding: 8px;
margin-top: 16px;
border-radius: 8px;
font-weight: normal;
}
.appsPane {
justify-content: center;
margin-top: 16px;
padding: 0 16px;
b {
cursor: pointer;
font-size: 24px;
background: $uni-color-primary;
color: #fff;
padding: 8px;
margin: 4px;
border-radius: 8px;
font-weight: normal;
}
}
}
</style>

96
src/pages/login.vue Normal file
View File

@@ -0,0 +1,96 @@
<template>
<section class="login">
<u-form :model="form" ref="loginForm" label-width="140">
<u-form-item label="账号" prop="phone" :errory-type="['message']">
<u-input v-model="form.phone" placeholder="请输入手机号"/>
</u-form-item>
<u-form-item label="验证码" prop="vcode">
<u-input v-model="form.vcode" placeholder="请输入短信验证码"/>
<u-verification-code ref="vcode" secords="60" @change="v=>tips=v"/>
<div class="vcode" @tap="$u.debounce(getVCode)">{{ tips }}</div>
</u-form-item>
</u-form>
<div bottom>
<u-button type="primary" @tap="handleLogin">绑定并登录</u-button>
</div>
</section>
</template>
<script>
import {mapActions, mapState} from "vuex";
export default {
name: "login",
inject: ['root'],
computed: {
...mapState(['lastPage']),
rules() {
return {
phone: [{required: true, message: "请选择分组"}],
vcode: [{required: true, message: "请选择快捷回复类型"}],
}
}
},
data() {
return {
form: {},
tips: ''
}
},
methods: {
...mapActions(['getCode']),
getVCode() {
if (this.form.phone) {
this.$http.post("/admin/user/sendCode", null, {
withoutToken: 1,
params: {phone: this.form.phone}
}).then(() => {
this.$u.toast("验证已发送!")
this.$refs.vcode?.start()
})
} else {
this.$u.toast("请先填写手机号!")
}
},
handleLogin() {
this.$refs.loginForm.validate(v => {
if (v) {
let params = {
...this.form, code: this.$route.query?.code, then: res => {
let last = uni.getStorageSync("lastApp")
if (last) {
this.$store.commit("login", [res?.token_type, res?.access_token].join(" ").trim())
uni.removeStorageSync("lastApp")
// this.root.getCode(location.origin + last)
uni.reLaunch({url: "./loading?app=" + last})
} else this.$u.toast("绑定成功,请重新打开应用页面!")
}
}
this.$store.commit("bindAccount", params)
}
})
}
},
created() {
!this.$route.query?.code && this.getCode()
},
mounted() {
this.$nextTick(() => this.$refs.loginForm?.setRules(this.rules))
}
}
</script>
<style lang="scss" scoped>
.login {
border-top: 1px solid #D4D4D4;
padding: 0 0 208px;
background: #F5F5F5;
box-sizing: border-box;
height: 100%;
.vcode {
color: $uni-color-primary;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,398 @@
<template>
<div class="add-meeting">
<div v-if="!userSelect">
<div class="card">
<header><em>*</em>会议标题</header>
<input v-model="form.title" placeholder="请输入" :maxlength="30">
</div>
<div class="card">
<header><em>*</em>起止时间</header>
<u-row justify="between">
<div class="time" @click="pick(0)">
<span>{{form.startTime.time}}</span>
<span>{{form.startTime.year}}{{form.startTime.month}}{{form.startTime.day}} {{form.startTime.weekday}}</span>
</div>
<div class="arrow"></div>
<div class="time" @click="pick(1)">
<span>{{form.endTime.time}}</span>
<span>{{form.endTime.year}}{{form.endTime.month}}{{form.endTime.day}} {{form.endTime.weekday}}</span>
</div>
</u-row>
</div>
<div class="card">
<header><em>*</em>会议地点</header>
<input v-model="form.address" placeholder="请输入" :maxlength="30">
</div>
<div class="card">
<header><em>*</em>会议内容</header>
<textarea v-model="form.content" placeholder="请输入" :maxlength="500"></textarea>
</div>
<div class="card">
<AiUploader :multiple="true" type="file" :limit="9" placeholder="上传附件" @list="fileList" :def="form.files"></AiUploader>
</div>
<div class="card item-wrap" @click="select">
<u-row justify="between" class="item" @click="userSelect=true">
<header><em>*</em>参会人</header>
<div class="right">
<template v-if="!form.attendees.length">
<span>请选择</span>
</template>
<template v-else>
已选择<em>{{form.attendees.map(e=>e.name).slice(0,2).join("、")}}</em><em>{{form.attendees.length}}</em>
</template>
<div class="right-arrow"></div>
</div>
</u-row>
</div>
<div class="card item-wrap" style="margin-top: 0">
<u-row justify="between" class="item">
<header>参会提醒</header>
<picker class="right" @change="beforeNoticeChange" :value="form.noticeBefore" range-key="dictName"
:range="$dict.getDict('meetingNoticeBefore')">
<span>{{ form.noticeBefore !=null ? $dict.getDict('meetingNoticeBefore')[form.noticeBefore]["dictName"] : "请选择"}}</span>
<div class="right-arrow"></div>
</picker>
</u-row>
</div>
<div class="card item-wrap" style="margin-top: 0">
<u-row justify="between" class="item">
<header>确认提醒</header>
<picker class="right" @change="afterNoticeChange" :value="form.noticeAfter" range-key="dictName"
:range="$dict.getDict('meetingNoticeAfter')">
<span>{{form.noticeAfter !=null ? $dict.getDict('meetingNoticeAfter')[form.noticeAfter]["dictName"] : "请选择"}}</span>
<div class="right-arrow"></div>
</picker>
</u-row>
</div>
<div class="footer">
<div @click="add(0)">保存草稿</div>
<div @click="add(1)">发布会议</div>
</div>
</div>
<u-picker mode="time" v-model="show" :params="params" @confirm="confirm"></u-picker>
<AiBack ref="aiBack" v-if="!userSelect"/>
<AiSelectEnterprise :visible.sync="userSelect" :value="form.attendees" v-if="userSelect" @change="change"></AiSelectEnterprise>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
import AiSelectEnterprise from "../../../components/AiSelectEnterprise/AiSelectEnterprise";
import AiUploader from "../../../components/AiUploader";
export default {
name: "addMeeting",
components: {AiBack,AiSelectEnterprise,AiUploader},
data() {
const initTime = {
time: "",
year: "",
month: "",
day: "",
weekday: "",
timestamp: "",
}
return {
show: false,
index: 0,
list: [],
form: {
id: null,
title: "",
startTime: {...initTime},
endTime: {...initTime},
address: "",
content: "",
attendees: [],
noticeBefore: 4,
noticeAfter: 0,
files: [],
},
userSelect: false,
}
},
onLoad(opt) {
if(opt.id) {
this.form.id = opt.id
this.getDetail()
}
this.$dict.load("meetingNoticeBefore", "meetingNoticeAfter");
this.$nextTick(()=>{
let date = new Date();
this.form.startTime.time = date.getHours()?.toString()?.padStart(2, "0") + ":" + date.getMinutes()?.toString()?.padStart(2, "0")
this.form.startTime.year = date.getFullYear()
this.form.startTime.month = (date.getMonth()+1)?.toString()?.padStart(2, "0")
this.form.startTime.day = date.getDate()
this.form.startTime.weekday = '日一二三四五六'.charAt(date.getDay())
this.form.endTime = {...this.form.startTime}
})
},
computed: {
params() {
return {
year: true,
month: true,
day: true,
hour: true,
minute: true,
timestamp: true,
}
}
},
methods: {
fileList(e){
this.form.files = e
},
change(e){
this.form.attendees = e
},
beforeNoticeChange(e) {
this.form.noticeBefore = e.detail.value
},
afterNoticeChange(e) {
this.form.noticeAfter = e.detail.value
},
getDetail(){
this.$http.post("/app/appmeetinginfo/info-id",null,{
params:{
id:this.form.id
}
}).then(res=>{
if (res && res.data) {
this.form.title = res.data.title
this.form.address = res.data.address
this.form.content = res.data.content
this.form.attendees = res.data.attendees
this.form.noticeBefore = res.data.noticeBefore
this.form.noticeAfter = res.data.noticeAfter
this.form.files = res.data.files
this.form.startTime.time = res.data.startTime.split(" ")[1].substr(0, 5)
this.form.startTime.year = res.data.startTime.split(" ")[0].split("-")[0]
this.form.startTime.month = res.data.startTime.split(" ")[0].split("-")[1]
this.form.startTime.day = res.data.startTime.split(" ")[0].split("-")[2]
this.form.startTime.weekday = '日一二三四五六'.charAt(new Date(res.data.startTime.split(" ")[0]).getDay())
this.form.startTime.timestamp = new Date(res.data.startTime).getTime()
this.form.endTime.time = res.data.endTime.split(" ")[1].substr(0, 5)
this.form.endTime.year = res.data.endTime.split(" ")[0].split("-")[0]
this.form.endTime.month = res.data.endTime.split(" ")[0].split("-")[1]
this.form.endTime.day = res.data.endTime.split(" ")[0].split("-")[2]
this.form.endTime.weekday = '日一二三四五六'.charAt(new Date(res.data.endTime.split(" ")[0]).getDay())
this.form.endTime.timestamp = new Date(res.data.endTime).getTime()
}
})
},
confirm(e) {
if (new Date().getTime() / 1000 > e.timestamp) return this.$u.toast("选择时间不能小于当前时间")
if (this.index == 0) {
this.form.startTime = {...e}
this.form.startTime.time = e.hour + ":" + (e.minute.length > 1 ? e.minute : ("0" + e.minute))
this.form.startTime.weekday = '日一二三四五六'.charAt(new Date(e.timestamp * 1000).getDay())
} else {
if (this.form.startTime.timestamp >= e.timestamp) {
return this.$u.toast("结束时间不能小于开始时间");
}
this.form.endTime = {...e}
this.form.endTime.time = e.hour + ":" + (e.minute.length > 1 ? e.minute : ("0" + e.minute))
this.form.endTime.weekday = '日一二三四五六'.charAt(new Date(e.timestamp * 1000).getDay())
}
},
add(status) {
if(status==1){
if (!this.form.title) return this.$u.toast("请输入会议标题")
if (this.form.startTime.timestamp >= this.form.endTime.timestamp) return this.$u.toast("结束时间不能小于开始时间")
if (!this.form.address) return this.$u.toast("请输入会议地点")
if (!this.form.content) return this.$u.toast("请输入会议内容")
if(!this.form.attendees.length) return this.$u.toast("请选择参会人")
}
this.$http.post("/app/appmeetinginfo/add-update", {
...this.form,
files:this.form.files.map(e=>e.id),
status,
startTime: this.form.startTime.year + "-" + this.form.startTime.month + "-" + this.form.startTime.day + " " + this.form.startTime.time + ":00",
endTime: this.form.endTime.year + "-" + this.form.endTime.month + "-" + this.form.endTime.day + " " + this.form.endTime.time + ":00",
}).then(res => {
if (res.code == 0) {
this.$u.toast(status == 1 ? "发布成功" : "保存成功")
this.$refs["aiBack"].back()
}
})
},
pick(index) {
this.index = index
this.show = true
},
select() {
uni.navigateTo({
url: "/pages/meetingNotice/components/notice"
})
}
},
}
</script>
<style lang="scss" scoped>
.add-meeting {
min-height: 100%;
background: #F5F5F5;
padding-bottom: 140px;
.card {
background-color: #FFFFFF;
box-sizing: border-box;
padding: 32px;
margin-top: 16px;
header {
font-size: 32px;
font-weight: 400;
color: #333333;
em {
font-style: normal;
font-size: 32px;
color: #FF4466;
margin-right: 8px;
vertical-align: middle;
}
}
input {
margin: 32px 0 16px;
box-sizing: border-box;
padding: 0 16px;
}
textarea {
width: 100%;
height: 160px;
margin: 32px 0 16px;
box-sizing: border-box;
padding: 0 16px;
}
.u-row {
margin-top: 34px;
.time {
display: flex;
flex-direction: column;
& > span:first-child {
font-size: 60px;
font-weight: 600;
color: #333333;
line-height: 84px;
}
& > span:last-child {
font-size: 22px;
color: #333333;
}
}
.arrow {
width: 28px;
height: 68px;
overflow: hidden;
position: relative;
transform: rotate(180deg);
&:before, &:after {
content: "";
width: 50px;
height: 50px;
position: absolute;
transform: scaleY(1.3) translate(30%, -40px) rotate(45deg);
}
&:before {
top: 59px;
background-color: #CCCCCC;
}
&:after {
left: 7px;
top: 59px;
background-color: #FFFFFF;
}
}
}
.item {
height: 112px;
box-shadow: 0px -1px 0px 0px #D8DDE6;
margin-top: 0;
.right {
font-size: 28px;
color: #999999;
display: flex;
align-items: center;
em {
font-style: normal;
color: #1365DD;
}
}
.right-arrow {
width: 16px;
height: 16px;
display: inline-block;
border-top: 5px solid #CCCCCC;
border-right: 5px solid #CCCCCC;
transform: rotate(45deg);
}
}
}
.item-wrap {
padding: 0 32px;
}
.footer {
height: 112px;
width: 100%;
position: fixed;
left: 0;
bottom: 0;
background-color: #FFFFFF;
display: flex;
align-items: center;
& > div {
font-size: 36px;
color: #333333;
}
& > div:first-child {
width: 270px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
& > div:last-child {
width: calc(100% - 270px);
height: 100%;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
background-color: #1365DD;
}
}
}
</style>

View File

@@ -0,0 +1,231 @@
<template>
<div class="belong-to-me">
<ai-top-fixed>
<u-tabs :list="tabs" :is-scroll="false" :current="index" bar-width="88" :height="96" @change="change"></u-tabs>
</ai-top-fixed>
<div class="body">
<template v-if="list.length">
<div class="card" v-for="(item,index) in list" :key="index" @click="handleClick(item)">
<header>{{item.title}}</header>
<u-row justify="between">
<div class="time">
<span>{{item.startTime|formatTime}}</span>
<span>{{item.startTime|formatDate(0)}}{{item.startTime|formatDate(1)}}{{item.startTime|formatDate(2)}} {{item.startTime|formatWeek}}</span>
</div>
<div class="arrow"></div>
<div class="time">
<span>{{item.endTime|formatTime}}</span>
<span>{{item.endTime|formatDate(0)}}{{item.endTime|formatDate(1)}}{{item.endTime|formatDate(2)}} {{item.endTime|formatWeek}}</span>
</div>
</u-row>
<u-row class="info">
<span>发起单位</span>
<span>{{item.unitName}}</span>
</u-row>
<u-gap height="20"></u-gap>
<u-row class="info">
<span>会议地点</span>
<span>{{item.address}}</span>
</u-row>
<div class="tag" :style="{background:'url(' + tag(item.status) + ')'}"></div>
</div>
</template>
<template v-else>
<ai-empty/>
</template>
</div>
<AiBack/>
</div>
</template>
<script>
import AiTopFixed from "../../../components/AiTopFixed";
import AiBack from "../../../components/AiBack";
import AiEmpty from "../../../components/AiEmpty/AiEmpty";
export default {
name: "belongToMe",
components: {AiTopFixed, AiBack, AiEmpty},
data() {
return {
index: 0,
current: 1,
list: [],
status: "加载更多",
}
},
onLoad() {
this.getList()
},
computed: {
tabs() {
return [
{name: "全部"},
{name: "未开始"},
{name: "进行中"},
{name: "已取消"},
{name: "已结束"},
]
}
},
methods: {
tag(status) {
return {
"1": this.$cdn + 'common/1wks.png',
"2": this.$cdn + 'common/1jxz.png',
"3": this.$cdn + 'common/1yqx.png',
"4": this.$cdn + 'common/1yjs.png'
}[status]
},
getList() {
this.$http.post("/app/appmeetinginfo/list", null, {
params: {
listType: 0,
meetingStatus: this.index == 0 ? "-1" : this.index,
size: 10,
current: this.current
}
}).then(res => {
if (res && res.data) {
if (this.current > 1 && this.current > res.data.pages) {
this.status = "已经到底啦"
}
this.list = this.current > 1 ? [...this.list, ...res.data.records] : res.data.records
}
})
},
handleClick({id}) {
uni.navigateTo({
url: "/pages/meetingNotice/components/detail?id=" + id
})
},
change(e) {
this.index = e
this.current = 1
this.getList()
},
},
onReachBottom() {
this.current = this.current + 1;
this.getList()
},
filters: {
formatTime(date) {
return date.split(" ")[1]?.substr(0, 5)
},
formatDate(date, i) {
return date.split(" ")[0]?.split("-")[i]
},
formatWeek(date) {
return "日一二三四五六".charAt(new Date(date.split(" ")[0]).getDay())
},
}
}
</script>
<style lang="scss" scoped>
.belong-to-me {
min-height: 100%;
background-color: #F5F5F5;
::v-deep .content {
padding: 0 !important;
}
.body {
box-sizing: border-box;
padding: 40px 32px;
.card {
background-color: #FFFFFF;
box-sizing: border-box;
padding: 32px;
border-radius: 8px;
margin-bottom: 32px;
position: relative;
&:last-child {
margin-bottom: 0;
}
& > header {
font-size: 32px;
font-weight: 600;
color: #333333;
}
.time {
display: flex;
flex-direction: column;
margin: 46px 0;
& > span:first-child {
font-size: 60px;
font-weight: 600;
color: #333333;
line-height: 84px;
}
& > span:last-child {
font-size: 22px;
color: #333333;
}
}
.arrow {
width: 28px;
height: 68px;
overflow: hidden;
position: relative;
transform: rotate(180deg);
&:before, &:after {
content: "";
width: 50px;
height: 50px;
position: absolute;
transform: scaleY(1.3) translate(30%, -40px) rotate(45deg);
}
&:before {
top: 59px;
background-color: #CCCCCC;
}
&:after {
left: 7px;
top: 59px;
background-color: #FFFFFF;
}
}
.info {
flex-wrap: nowrap;
& > span:first-child {
font-size: 30px;
color: #999999;
flex-shrink: 0;
}
& > span:last-child {
font-size: 30px;
color: #343D65;
}
}
.tag {
width: 112px;
height: 112px;
background-repeat: no-repeat;
background-size: 100% 100% !important;
position: absolute;
top: 0;
right: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,478 @@
<template>
<div class="detail">
<template v-if="!list">
<div class="card">
<header>{{ detail.title }}</header>
<u-gap height="16"></u-gap>
<u-row>
<u-avatar :src="$cdn + 'common/xzh.png'" v-if="false"></u-avatar>
<div class="u-avatar__img" v-else>{{ detail.userName && detail.userName.substr(-2) }}</div>
<span>{{ detail.userName }}</span>
</u-row>
<u-gap height="32"></u-gap>
<u-row>
<img :src="$cdn + 'common/meeting.png'" alt="">
<span :style="{color:color(detail.status)}">{{ $dict.getLabel('meetStatus', detail.status) }}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<img :src="$cdn + 'common/date.png'" alt="">
<span>{{ detail.startTime && detail.startTime.substr(0, 16) }} {{ detail.endTime && detail.endTime.substr(0, 16) }}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<img :src="$cdn + 'common/location.png'" alt="">
<span>{{ detail.address }}</span>
</u-row>
</div>
<div class="card">
<span>{{ detail.content }}</span>
</div>
<div class="card" v-if="detail.files && detail.files.length">
<div class="label">相关附件</div>
<div class="file" v-for="(item,index) in detail.files" @click="preFile(item)" :key="index">
<u-row justify="between">
<label class="left">
<img :src="$cdn + 'common/appendix.png'" alt="">
<span>{{ item.fileName }}.{{ item.postfix }}</span>
</label>
<span>{{ item.fileSizeStr }}</span>
</u-row>
</div>
</div>
<div class="card item-wrap">
<u-row justify="between">
<span>参会人</span>
<label class="right" @click="list=true">
查看全部<em>{{ detail.attendees && detail.attendees.length }}</em>
<div class="right-arrow"></div>
</label>
</u-row>
</div>
<div class="footer" v-if="['1','2'].includes(detail.status) && detail.joinStatus==0">
<div @click="updateStatus(0)">请假</div>
<div @click="updateStatus(1)">确认会议</div>
</div>
<div class="footer" v-if="['1','2'].includes(detail.status) && detail.joinStatus!=0">
<label>{{ detail.joinStatus|transform }}</label>
<img :src="$cdn + tag(detail.joinStatus)" alt="">
</div>
</template>
<template v-else>
<div class="att-list">
<AiTopFixed>
<u-tabs :list="tabs" :current="current" height="96" :is-scroll="false" bar-width="192"
@change="change"></u-tabs>
</AiTopFixed>
<div v-for="(item,index) in detail.attendees && detail.attendees.filter(e=>e.joinStatus==current)" :key="index"
class="att-wrap">
<div class="left">
<u-avatar :src="item.avatar || (($cdn + 'common/xztx.png'))" size="74" mode="square"></u-avatar>
<text class="name" style="margin-left: 8px">{{ item.name }}</text>
</div>
<img :src="$cdn + 'common/phone.png'" alt="" @click="call(item)">
</div>
</div>
</template>
<AiBack visible eventName="back" @back="list=false"/>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
import {mapActions} from "vuex";
import AiTopFixed from "../../../components/AiTopFixed";
export default {
name: "detail",
components: {AiBack, AiTopFixed},
data() {
return {
id: null,
detail: {},
list: false,
current: 0,
}
},
computed: {
tabs() {
return [
{name: this.count(0) + "人未确认"},
{name: this.count(1) + "人已确认"},
{name: this.count(2) + "人已请假"},
]
},
},
onLoad(opt) {
this.id = opt.id
this.$dict.load("meetStatus").then(_ => this.getDetail())
},
methods: {
count(sta) {
return this.detail.attendees?.filter(e => e.joinStatus == sta)?.length;
},
change(index) {
this.current = index;
},
call(item) {
if (item.phone) {
uni.makePhoneCall({
phoneNumber: item.phone
});
}
},
...mapActions(['previewFile', 'injectJWeixin']),
preFile(e) {
if ([".jpg", ".png", ".gif"].includes(e.postfix.toLowerCase())) {
uni.previewImage({
current: e.url,
urls: [e.url]
})
} else {
this.previewFile({...e})
}
},
tag(status) {
return {
"1": "common/2confirmed2.png",
"2": "common/2absent2.png"
}[status]
},
updateStatus(status) {
this.$http.post(status == 0 ? "/app/appmeetinginfo/absent" : "/app/appmeetinginfo/confirm", null, {
params: {
meetingId: this.id,
reason: status == 0 ? "" : null
}
}).then(res => {
if (res.code == 0) {
this.$u.toast(status == 0 ? "请假成功" : "确认成功")
this.getDetail()
}
})
},
color(status) {
if (status == 1) {
return "#FF8822"
}
if (status == 2) {
return "#1365DD"
}
if (status == 3) {
return "#FF4466"
}
return "#343D65"
},
getDetail() {
this.$http.post("/app/appmeetinginfo/info-id", null, {
params: {id: this.id}
}).then(res => {
if (res && res.data) {
this.detail = res.data
}
})
}
},
filters: {
transform(status) {
if (status == 1) {
return "已确认"
}
if (status == 2) {
return "已请假"
}
}
}
}
</script>
<style lang="scss" scoped>
.detail {
min-height: 100%;
background-color: #F5F5F5;
padding-bottom: 140px;
::v-deep .AiTopFixed {
margin-bottom: 16px;
.content {
padding: 0 !important;
}
}
.att-list {
min-height: 100%;
.att-wrap {
display: flex;
height: 112px;
align-items: center;
justify-content: space-between;
background-color: #ffffff;
box-sizing: border-box;
padding: 0 50px;
border-bottom: 1px solid #E4E5E6;
.left {
display: flex;
align-items: center;
&:after {
content: "";
position: absolute;
right: 0;
bottom: 0;
width: 622px;
height: 2px;
background-color: rgba(216, 221, 230, 0.5);
}
.name {
font-size: 30px;
font-weight: 600;
color: #333333;
}
}
& > img {
width: 48px;
height: 48px;
}
}
}
.card {
background-color: #FFFFFF;
margin-bottom: 8px;
box-sizing: border-box;
padding: 16px 32px;
header {
font-size: 40px;
font-weight: 600;
color: #333333;
line-height: 64px;
letter-spacing: 1px;
}
.u-row {
& > div {
background-color: #2266FF;
border-radius: 50%;
text-align: center;
font-size: 22px;
font-weight: bold;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
& > span {
font-size: 30px;
color: #343D65;
margin-left: 16px;
}
::v-deep .u-avatar__img {
width: 56px;
height: 56px;
vertical-align: middle;
}
img {
width: 48px;
height: 48px;
}
}
& > span {
font-size: 32px;
color: #333333;
line-height: 48px;
letter-spacing: 1px;
display: inline-block;
box-sizing: border-box;
padding: 16px 0;
}
.label {
height: 96px;
font-size: 32px;
color: #333333;
display: flex;
align-items: center;
margin-bottom: 16px;
}
.file {
height: 128px;
background: #FFFFFF;
border-radius: 8px;
border: 1px solid #CCCCCC;
box-sizing: border-box;
padding: 0 16px;
margin-bottom: 32px;
& > .u-row {
height: 100%;
.left {
width: 522px;
display: flex;
align-items: center;
& > img {
width: 96px;
height: 96px;
}
& > span {
font-size: 32px;
color: #333333;
display: inline-block;
line-height: 44px;
}
}
& > span {
font-size: 28px;
color: #999999;
}
}
}
.active {
background-color: #F3F6F9;
}
.name {
font-size: 32px;
font-weight: 400;
color: #333333;
}
.wrap {
height: 112px;
display: flex;
align-items: center;
position: relative;
&:after {
content: "";
position: absolute;
right: 0;
bottom: 0;
width: 622px;
height: 2px;
background-color: rgba(216, 221, 230, 0.5);
}
& > label {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #4E8EEE;
font-size: 28px;
font-weight: 600;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.item-wrap {
height: 112px;
padding: 0 32px;
.u-row {
height: 100%;
& > span {
font-size: 32px;
font-weight: 400;
color: #333333;
}
}
.right {
font-size: 28px;
color: #999999;
display: flex;
align-items: center;
em {
font-style: normal;
color: #1365DD;
}
.right-arrow {
width: 16px;
height: 16px;
border-top: 5px solid #CCCCCC;
border-right: 5px solid #CCCCCC;
transform: rotate(45deg);
}
}
}
.footer {
height: 112px;
width: 100%;
position: fixed;
left: 0;
bottom: 0;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
& > div {
font-size: 36px;
color: #333333;
}
& > div:first-child {
width: 270px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
& > div:last-child {
width: calc(100% - 270px);
height: 100%;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
background-color: #1365DD;
}
& > label {
font-size: 36px;
font-weight: 400;
color: #999999;
}
img {
width: 158px;
height: 104px;
position: absolute;
right: 0;
top: 0;
}
}
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<div class="meeting-list">
<div class="card" v-for="(item,index) in list" :key="index" @click="detail(item)">
<header>
<span>{{item.title}}</span>
<span>
<span v-if="index==2">保存于</span>
{{item.createTime}}</span>
</header>
<u-row justify="between">
<div class="time">
<span>{{item.startTime|format}}</span>
<span>{{item.startTime|formatDate(0)}}{{item.startTime|formatDate(1)}}{{item.startTime|formatDate(2)}} {{item.startTime|formatWeek}}</span>
</div>
<div class="arrow"></div>
<div class="time">
<span>{{item.endTime|format}}</span>
<span>{{item.endTime|formatDate(0)}}{{item.endTime|formatDate(1)}}{{item.endTime|formatDate(2)}} {{item.endTime|formatWeek}}</span>
</div>
</u-row>
<u-row class="info">
<span>发起人员</span>
<span>{{item.userName}}</span>
</u-row>
<u-gap height="20"></u-gap>
<u-row class="info">
<span>会议地点</span>
<span>{{item.address}}</span>
</u-row>
<div class="tag" v-if="item.status!=0" :style="{background:'url(' + $cdn + tag(item.status) +')'}"></div>
</div>
<u-loadmore :status="status" v-if="list.length"/>
<AiEmpty v-if="!list.length"></AiEmpty>
<AiBack/>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
import AiEmpty from "../../../components/AiEmpty/AiEmpty";
export default {
name: "meetingList",
components: {AiBack, AiEmpty},
data() {
return {
index: null,
list: [],
current: 1,
status: "加载更多",
}
},
onLoad(opt) {
this.index = opt.index
uni.setNavigationBarTitle({
title: opt.index == 0 ? "历史会议" : "草稿箱"
});
},
methods: {
detail({id}) {
let url
if (this.index == 2) {
url = "/pages/meetingNotice/components/addMeeting?id=" + id
} else {
url = "/pages/meetingNotice/components/detail?id=" + id
}
uni.navigateTo({url})
},
tag(status) {
return {
"1": 'common/1wks.png',
"2": 'common/1jxz.png',
"3": 'common/1yqx.png',
"4": 'common/1yjs.png'
}[status]
},
getData() {
this.$http.post("/app/appmeetinginfo/list", null, {
params: {
listType: this.index == 0 ? "2" : '0',
meetingStatus: this.index == 0 ? "4" : "0",
size: 10,
current: this.current,
}
}).then(res => {
if (res && res.data) {
if (this.current > 1 && this.current > res.data.pages) {
this.status = "已经到底啦"
}
this.list = this.current > 1 ? [...this.list, ...res.data.records] : res.data.records
}
})
},
},
filters: {
format(date) {
return date.split(" ")[1].substr(0, 5)
},
formatDate(date, index) {
return date.split(" ")[0].split("-")[index]
},
formatWeek(date) {
return "日一二三四五六".charAt((new Date(date.split(" ")[0]).getDay()))
}
},
onShow(){
this.getData()
},
onReachBottom() {
this.current = this.current + 1;
this.getData()
},
}
</script>
<style lang="scss" scoped>
.meeting-list {
min-height: 100%;
background-color: #F5F5F5;
box-sizing: border-box;
padding: 32px;
.card {
background-color: #FFFFFF;
box-sizing: border-box;
padding: 32px;
border-radius: 8px;
margin-bottom: 32px;
position: relative;
&:last-child {
margin-bottom: 0;
}
& > header {
font-size: 32px;
font-weight: 600;
color: #333333;
display: flex;
flex-direction: column;
& > span:last-child {
font-size: 28px;
font-weight: 400;
color: #999999;
margin-top: 10px;
}
}
.time {
display: flex;
flex-direction: column;
margin: 46px 0;
& > span:first-child {
font-size: 60px;
font-weight: 600;
color: #333333;
line-height: 84px;
}
& > span:last-child {
font-size: 22px;
color: #333333;
}
}
.arrow {
width: 28px;
height: 68px;
overflow: hidden;
position: relative;
transform: rotate(180deg);
&:before, &:after {
content: "";
width: 50px;
height: 50px;
position: absolute;
transform: scaleY(1.3) translate(30%, -40px) rotate(45deg);
}
&:before {
top: 59px;
background-color: #CCCCCC;
}
&:after {
left: 7px;
top: 59px;
background-color: #FFFFFF;
}
}
.info {
flex-wrap: nowrap;
& > span:first-child {
flex-shrink: 0;
font-size: 30px;
color: #999999;
}
& > span:last-child {
font-size: 30px;
color: #343D65;
}
}
.tag {
width: 112px;
height: 112px;
background-repeat: no-repeat !important;
background-size: 100% 100% !important;
position: absolute;
top: 0;
right: 0;
}
}
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<div class="meeting">
<ai-top-fixed>
<u-grid :col="3" :border="false">
<u-grid-item v-for="(item,index) in grid" :key="index" :custom-style="{padding:'14px 0'}" @click="handleClick(index)">
<u-icon :name="item.icon" :size="64"></u-icon>
<view class="label">{{item.label}}</view>
</u-grid-item>
</u-grid>
</ai-top-fixed>
<div class="body">
<header>待参加的会议</header>
<template v-if="meetingList.length">
<div class="card" v-for="(item,index) in meetingList" :key="index" @click="detail(item)">
<header>{{item.title}}</header>
<u-row justify="between">
<div class="time">
<span>{{item.startTime|format}}</span>
<span>{{item.startTime|formatDate(0)}}{{item.startTime|formatDate(1)}}{{item.startTime|formatDate(2)}} {{item.startTime|formatWeek}}</span>
</div>
<div class="arrow"></div>
<div class="time">
<span>{{item.endTime|format}}</span>
<span>{{item.endTime|formatDate(0)}}{{item.endTime|formatDate(1)}}{{item.endTime|formatDate(2)}} {{item.endTime|formatWeek}}</span>
</div>
</u-row>
<u-row class="info">
<span>发起人员</span>
<span>{{item.userName}}</span>
</u-row>
<u-gap height="20"></u-gap>
<u-row class="info">
<span>会议地点</span>
<span>{{item.address}}</span>
</u-row>
<div class="tag" :style="{background:'url(' + $cdn + tag(item.joinStatus) + ')'}"></div>
</div>
</template>
<template v-else>
<ai-empty/>
</template>
</div>
<u-divider bg-color="#F5F5F5" v-if="meetingList.length">已经到底啦</u-divider>
<ai-add @add="add"/>
</div>
</template>
<script>
import AiEmpty from "../../components/AiEmpty/AiEmpty";
import AiTopFixed from "../../components/AiTopFixed";
import AiAdd from "../../components/AiAdd";
export default {
name: "meetingNotice",
components: {AiEmpty, AiTopFixed, AiAdd},
data() {
return {
meetingList:[]
}
},
computed: {
grid() {
return [
{
icon: this.$cdn + "/common/iconlshy.png",
label: "历史会议"
},
{
icon: this.$cdn + "/common/iconwfqd.png",
label: "我发起的"
},
{
icon: this.$cdn + "/common/iconcgx.png",
label: "草稿箱"
}
]
}
},
methods: {
tag(status){
return {
"0":"common/1wqr.png",
"1":"common/1yqr.png",
"2":"common/1yqj.png",
}[status]
},
detail({id}){
uni.navigateTo({
url:"/pages/meetingNotice/components/detail?id=" + id
})
},
getData() {
this.$http.post("/app/appmeetinginfo/list", null, {
params: {
listType: "1",
meetingStatus: "1|2",
size: 999
}
}).then(res=>{
if(res && res.data){
this.meetingList = res.data.records
}
})
},
handleClick(index){
let url
if(index==0 || index==2){
url="/pages/meetingNotice/components/meetingList?index=" + index
}else if(index==1){
url="/pages/meetingNotice/components/belongToMe"
}
uni.navigateTo({url})
},
add() {
uni.navigateTo({
url:"/pages/meetingNotice/components/addMeeting"
})
}
},
filters:{
format(date){
return date.split(" ")[1].substr(0,5)
},
formatDate(date,index){
return date.split(" ")[0].split("-")[index]
},
formatWeek(date){
return "日一二三四五六".charAt((new Date(date.split(" ")[0]).getDay()))
}
},
onShow(){
this.getData()
}
}
</script>
<style lang="scss" scoped>
.meeting {
min-height: 100%;
background: #F5F5F5;
padding-bottom: 48px;
.label {
font-size: 28px;
font-weight: 400;
color: #333333;
line-height: 48px;
margin-top: 8px;
}
.body {
box-sizing: border-box;
padding: 40px 32px;
& > header {
font-size: 36px;
font-weight: 600;
color: #333333;
margin-bottom: 38px;
}
.card {
background-color: #FFFFFF;
box-sizing: border-box;
padding: 32px;
border-radius: 8px;
margin-bottom: 32px;
position: relative;
&:last-child {
margin-bottom: 0;
}
& > header {
font-size: 32px;
font-weight: 600;
color: #333333;
}
.time {
display: flex;
flex-direction: column;
margin: 46px 0;
& > span:first-child {
font-size: 60px;
font-weight: 600;
color: #333333;
line-height: 84px;
}
& > span:last-child {
font-size: 22px;
color: #333333;
}
}
.arrow {
width: 28px;
height: 68px;
overflow: hidden;
position: relative;
transform: rotate(180deg);
&:before, &:after {
content: "";
width: 50px;
height: 50px;
position: absolute;
transform: scaleY(1.3) translate(30%, -40px) rotate(45deg);
}
&:before {
top: 59px;
background-color: #CCCCCC;
}
&:after {
left: 7px;
top: 59px;
background-color: #FFFFFF;
}
}
.info {
& > span:first-child {
font-size: 30px;
color: #999999;
}
& > span:last-child {
font-size: 30px;
color: #343D65;
}
}
.tag{
width: 112px;
height: 112px;
background-repeat: no-repeat !important;
background-size: 100% 100% !important;
position: absolute;
top: 0;
right: 0;
}
}
}
::v-deep .content{
padding: 0 !important;
}
}
</style>

View File

@@ -0,0 +1,358 @@
<template>
<div class="add-meeting">
<div v-if="!userSelect">
<div class="card">
<header><em>*</em>公告标题</header>
<input v-model="form.title" placeholder="请输入" :maxlength="30">
</div>
<div class="card">
<header><em>*</em>公告内容</header>
<textarea v-model="form.content" placeholder="请输入" :maxlength="500"></textarea>
</div>
<div class="card">
<AiUploader :multiple="true" type="image" :limit="9" placeholder="上传图片" @list="fileList" :def="form.files"></AiUploader>
</div>
<div class="card item-wrap" @click="select">
<u-row justify="between" class="item" style="border-bottom: 1px solid #eeeeee" @click="userSelect=true">
<header><em>*</em>发送对象</header>
<div class="right">
<template v-if="!form.persons.length">
<span>请选择</span>
</template>
<template v-else>
已选择<em>{{form.persons.map(e=>e.name).slice(0,2).join("、")}}</em><em>{{form.persons.length}}</em>
</template>
<div class="right-arrow"></div>
</div>
</u-row>
<u-row justify="between" class="item" @click="userSelect=true">
<header><em>*</em>发送时间</header>
</u-row>
<u-row justify="between">
<div class="type" :class="[index==0 && 'active']" @click="index=0,form.releaseTime=null">立即发送
<img :src="$cdn + 'notice/jiaobiao.png'" alt="" v-show="index==0">
</div>
<div class="type" :class="[index==1 && 'active']" @click="index=1">定时发送
<img :src="$cdn + 'notice/jiaobiao.png'" alt="" v-show="index==1">
</div>
</u-row>
<u-gap height="38"></u-gap>
<u-row justify="between" class="item" style="box-shadow: none;" @click="show=true" v-show="index==1">
<header><em>*</em>定时发送时间</header>
<div class="right">
<template v-if="!form.releaseTime">
<span>请选择</span>
</template>
<template v-else>
<span>{{form.releaseTime}}</span>
</template>
<div class="right-arrow"></div>
</div>
</u-row>
</div>
<div class="footer">
<div @click="add(0)">保存草稿</div>
<div @click="add(1)">立即发布</div>
</div>
</div>
<AiBack ref="aiBack" v-if="!userSelect"/>
<u-picker v-model="show" mode="time" :params="params" @confirm="confirm"></u-picker>
<AiSelectEnterprise :visible.sync="userSelect" :value="form.persons" v-if="userSelect" @change="change"></AiSelectEnterprise>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
import AiSelectEnterprise from "../../../components/AiSelectEnterprise/AiSelectEnterprise";
import AiUploader from "../../../components/AiUploader";
export default {
name: "add",
components: {AiBack,AiSelectEnterprise,AiUploader},
data() {
return {
show: false,
index: 0,
list: [],
form: {
id: null,
title: "",
content: "",
persons: [],
releaseTime:null,
files: [],
},
userSelect: false,
params: {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: true,
timestamp: true,
},
flag: null,
}
},
onLoad(opt) {
if(opt.id) {
this.form.id = opt.id;
this.flag = opt.flag;
this.getDetail();
}
},
methods: {
confirm(e){
if(e.timestamp< (Date.now()/1000)|0){
return this.$u.toast("发送时间不能小于当前时间");
}
this.form.releaseTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
},
fileList(e){
this.form.files = e
},
change(e){
this.form.persons = e
},
getDetail(){
this.$http.post("/app/appannouncement/detail",null,{
params:{
id:this.form.id,
detail: this.flag
}
}).then(res=>{
if (res && res.data) {
this.form.releaseTime = res.data.releaseTime;
Object.keys(this.form).map(e=>{
this.form[e] = res.data[e];
})
this.index = res.data.releaseTime ? 1 : 0;
}
})
},
add(status) {
if(status==1){
if (!this.form.title) return this.$u.toast("请输入会议标题")
if (!this.form.content) return this.$u.toast("请输入会议内容")
if(!this.form.persons.length) return this.$u.toast("请选择发送对象")
if(this.index==1 && !this.form.releaseTime) return this.$u.toast("请选择定时发送时间")
if(this.form.releaseTime && new Date(this.form.releaseTime).getTime() < Date.now()) return this.$u.toast("发送时间不能小于当前时间");
}
this.$http.post("/app/appannouncement/addOrUpdate", {
...this.form,
status,
}).then(res => {
if (res.code == 0) {
this.$u.toast(status == 1 ? "发布成功" : "保存成功")
this.$refs["aiBack"].back()
}
})
},
select() {
uni.navigateTo({
url: "/pages/meetingNotice/components/notice"
})
}
},
computed:{
background(){
return `url(${this.$cdn}/notice/jiaobiao.png) no-repeat; background-size: 46px 48px;position: absolute;bottom: 0;right: 0;`
},
}
}
</script>
<style lang="scss" scoped>
.add-meeting {
min-height: 100%;
background: #F5F5F5;
padding-bottom: 140px;
.card {
background-color: #FFFFFF;
box-sizing: border-box;
padding: 32px;
margin-top: 16px;
header {
font-size: 32px;
font-weight: 400;
color: #333333;
em {
font-style: normal;
font-size: 32px;
color: #FF4466;
margin-right: 8px;
vertical-align: middle;
}
}
input {
margin: 32px 0 16px;
box-sizing: border-box;
padding: 0 16px;
}
textarea {
width: 100%;
height: 160px;
margin: 32px 0 16px;
box-sizing: border-box;
padding: 0 16px;
}
.u-row {
margin-top: 34px;
.time {
display: flex;
flex-direction: column;
& > span:first-child {
font-size: 60px;
font-weight: 600;
color: #333333;
line-height: 84px;
}
& > span:last-child {
font-size: 22px;
color: #333333;
}
}
.arrow {
width: 28px;
height: 68px;
overflow: hidden;
position: relative;
transform: rotate(180deg);
&:before, &:after {
content: "";
width: 50px;
height: 50px;
position: absolute;
transform: scaleY(1.3) translate(30%, -40px) rotate(45deg);
}
&:before {
top: 59px;
background-color: #CCCCCC;
}
&:after {
left: 7px;
top: 59px;
background-color: #FFFFFF;
}
}
.type{
width: 320px;
height: 112px;
background: #F5F5F5;
color: #333333;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
font-weight: 500;
letter-spacing: 1px;
position: relative;
& > img{
width: 46px;
height: 48px;
position: absolute;
right: 0;
bottom: 0;
}
}
.active{
background-color: #E7F1FE;
color: #1174FE;
}
}
.item {
height: 112px;
box-shadow: 0px -1px 0px 0px #D8DDE6;
margin-top: 0;
.right {
font-size: 28px;
color: #999999;
display: flex;
align-items: center;
em {
font-style: normal;
color: #1365DD;
}
}
.right-arrow {
width: 16px;
height: 16px;
display: inline-block;
border-top: 5px solid #CCCCCC;
border-right: 5px solid #CCCCCC;
transform: rotate(45deg);
}
}
}
.item-wrap {
padding: 0 32px;
}
.footer {
height: 112px;
width: 100%;
position: fixed;
left: 0;
bottom: 0;
background-color: #FFFFFF;
display: flex;
align-items: center;
& > div {
font-size: 36px;
color: #333333;
}
& > div:first-child {
width: 270px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
& > div:last-child {
width: calc(100% - 270px);
height: 100%;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
background-color: #1365DD;
}
}
}
</style>

View File

@@ -0,0 +1,345 @@
<template>
<div class="detail">
<template v-if="detailObj">
<div class="card">
<header>{{detailObj.title}}</header>
<u-gap height="16"></u-gap>
<u-row>
<span>发布人</span>
<span>{{detailObj.releaseUserName}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>发布部门</span>
<span>{{detailObj.unitName}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>发布日期</span>
<span>{{detailObj.releaseTime}}</span>
</u-row>
<u-gap height="8"></u-gap>
</div>
<div class="card">
<div class="label">公告内容</div>
<u-parse :html="detailObj.content"></u-parse>
</div>
<div class="card" style="padding-top: 0" v-if="detailObj.files && detailObj.files.length">
<div class="label">相关附件</div>
<div class="file" v-for="(item,index) in detailObj.files" :key="index" @click="preFile(item)">
<u-row justify="between">
<label class="left">
<img :src="$cdn + 'common/appendix.png'" alt="">
<span>{{item.name}}.{{item.postfix}}</span>
</label>
<span>{{(item.size/1024).toFixed(2)}}KB</span>
</u-row>
</div>
</div>
<div class="card" @click="handleClick">
<u-row justify="between" class="item">
<span>接收对象</span>
<div class="right">
<em>{{detailObj.readNum}}</em>已读
<em>{{detailObj.unReadNum}}</em>未读
<div class="arrow"></div>
</div>
</u-row>
</div>
</template>
<AiEmpty description="该通知已撤回" v-else/>
<AiBack />
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
import {mapActions} from "vuex";
import AiEmpty from "../../../components/AiEmpty/AiEmpty";
export default {
name: "detail",
components:{AiBack,AiEmpty},
data() {
return {
detailObj: null,
id: null,
flag: null,
}
},
onLoad(opt){
this.id = opt.id;
this.flag = opt.flag;
this.getDetail();
},
methods: {
...mapActions(['previewFile', 'injectJWeixin']),
preFile(e) {
if([".jpg",".png",".gif"].includes(e.postfix.toLowerCase())){
uni.previewImage({
current: e.url,
urls: [e.url]
})
}else {
this.previewFile({...e})
}
},
getDetail() {
this.$http.post("/app/appannouncement/detail", null, {
params: {
id: this.id,
detail: this.flag
}
}).then(res=>{
if(res && res.data){
this.detailObj = res.data;
}
})
},
handleClick() {
uni.navigateTo({
url:"/pages/notification/components/read?id=" + this.id,
})
}
},
}
</script>
<style lang="scss" scoped>
.detail {
min-height: 100%;
background-color: #F5F5F5;
padding-bottom: 140px;
::v-deep .content {
padding: 0;
}
.card {
background-color: #FFFFFF;
margin-bottom: 8px;
box-sizing: border-box;
padding: 16px 32px;
header {
font-size: 40px;
font-weight: 600;
color: #333333;
line-height: 64px;
letter-spacing: 1px;
}
.u-row {
& > div {
border-radius: 50%;
text-align: center;
font-size: 22px;
font-weight: bold;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
& > span:first-child {
font-size: 30px;
color: #999999;;
line-height: 48px;
}
& > span:last-child {
font-size: 30px;
color: #343D65;
margin-left: 16px;
line-height: 48px;
}
.title {
width: 490px;
height: 112px;
display: flex;
align-items: center;
font-size: 32px;
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.right {
font-size: 28px;
display: flex;
align-items: center;
color: #666666;
.arrow {
width: 16px;
height: 16px;
border-top: 3px solid #CCCCCC;
border-right: 3px solid #CCCCCC;
transform: rotate(45deg);
}
}
}
.item {
position: relative;
height: 80px;
&:after {
width: 100%;
height: 1px;
background-color: rgba(216, 221, 230, 0.5);
content: "";
position: absolute;
left: 0;
bottom: 0;
}
}
& > span {
font-size: 32px;
color: #333333;
line-height: 48px;
letter-spacing: 1px;
display: inline-block;
}
.label {
height: 80px;
font-size: 32px;
color: #333333;
display: flex;
align-items: center;
margin-bottom: 16px;
& > em {
font-style: normal;
font-size: 32px;
color: #1365DD;
}
}
.file {
height: 128px;
background: #FFFFFF;
border-radius: 8px;
border: 1px solid #CCCCCC;
box-sizing: border-box;
padding: 0 16px;
margin-bottom: 32px;
& > .u-row {
height: 100%;
.left {
width: 476px;
display: flex;
align-items: center;
& > img {
flex-shrink: 0;
width: 96px;
height: 96px;
}
& > span {
font-size: 32px;
color: #333333;
display: inline-block;
line-height: 44px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
& > span {
font-size: 28px;
color: #999999;
}
}
}
.active {
background-color: #F3F6F9;
}
& > text {
width: 100%;
display: inline-block;
font-size: 30px;
color: #649EFD;
text-align: center;
}
.progress {
height: 12px;
background: #F2F4FC;
border-radius: 12px;
position: relative;
margin: 16px 0 64px 0;
.pro-active {
height: 12px;
background: #639EFD;
border-radius: 12px;
position: absolute;
left: 0;
top: 0;
}
}
em {
font-style: normal;
font-size: 28px;
color: #1365DD;
}
::v-deep .u-collapse {
position: relative;
&:after {
content: "";
width: 718px;
height: 1px;
background-color: rgba(216, 221, 230, 0.5);
position: absolute;
left: 0;
bottom: 0;
}
.u-collapse-head {
padding: 40px 0;
}
.u-collapse-content {
font-size: 32px;
color: #333333;
line-height: 48px;
letter-spacing: 1px;
}
}
}
.footer {
height: 112px;
width: 100%;
position: fixed;
left: 0;
bottom: 0;
background: #1365DD;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
color: #FFFFFF;
}
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div class="read">
<AiTopFixed>
<u-tabs :list="tabs" :is-scroll="false" height="96" bar-width="192" :current="current" @change="change"></u-tabs>
</AiTopFixed>
<div class="body">
<div class="item" v-for="(item,index) in (current==0 ? list.read : list.unRead)" :key="index">
<u-avatar :src="item.avatar" mode="square" size="76"></u-avatar>
<span class="name">{{item.name}}</span>
</div>
</div>
<AiBack/>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
import AiTopFixed from "../../../components/AiTopFixed";
export default {
name: "read",
components: {AiBack, AiTopFixed},
data() {
return {
current: 0,
id: null,
list: [],
}
},
onLoad(opt) {
this.id = opt.id;
this.getList();
},
methods: {
getList() {
this.$http.post("/app/appannouncementreader/list-unread",null,{
params:{
id:this.id
}
}).then(res => {
if (res && res.data) {
this.list = res.data;
}
})
},
change(val) {
this.current = val;
this.getList();
}
},
computed: {
tabs() {
return [
{name: this.list?.read?.length + "人已读"},
{name: this.list?.unRead?.length + "人未读"},
];
}
},
}
</script>
<style lang="scss" scoped>
.read {
min-height: 100%;
background-color: #F5F5F5;
::v-deep .content{
padding: 0 !important;
}
.body {
padding: 16px 0;
.item {
height: 120px;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 50px;
background-color: #ffffff;
border-bottom: 1px solid #eeeeee;
& > .name{
font-size: 36px;
font-weight: 600;
color: #333333;
line-height: 50px;
margin-left: 32px;
}
}
}
}
</style>

View File

@@ -0,0 +1,354 @@
<template>
<div class="notification">
<AiTopFixed>
<u-tabs :list="tabs" :is-scroll="false" height="96" bar-width="192" :current="index" @change="change"></u-tabs>
</AiTopFixed>
<div class="body" v-if="dataList.length">
<div class="card" v-for="(item,idx) in dataList" :key="idx" @click="handeClick(item)">
<template v-if="!item.imgUrl">
<label>
<b v-if="index==0 && item.readStatus==0"></b>
<div class="tag" v-if="index==1" :style="color(item.status)">{{$dict.getLabel("announcementStatus",item.status)}}</div>
{{item.title}}
</label>
<u-gap height="16"></u-gap>
<span class="info">
<text>{{item.releaseUserName}}</text>
<text>{{item.releaseTime}}</text>
</span>
</template>
<template v-else>
<div class="has-pic">
<div class="left">
<label>
<b v-if="index==0 && item.readStatus==0"></b>
<div class="tag" v-if="index==1" :style="color(item.status)">{{$dict.getLabel("announcementStatus",item.status)}}</div>
{{item.title}}
</label>
<u-gap height="16"></u-gap>
<span class="info">
<text>{{item.releaseUserName}}</text>
<text>{{item.releaseTime}}</text>
</span>
</div>
<img :src="item.imgUrl" alt="">
</div>
</template>
</div>
</div>
<AiEmpty v-else/>
<u-loadmore :status="status" v-if="dataList.length"/>
<AiAdd @add="add"/>
<u-popup v-model="show" mode="bottom">
<div class="popup-wrap">
<u-row justify="between">
<div class="colum" v-for="(item,index) in optList" :key="index" @click="handleOpt(item)">
<u-icon :name="item.icon" size="100" :custom-style="{backgroundColor:'#fff',borderRadius:'16px'}"></u-icon>
<u-gap height="16"></u-gap>
{{item.name}}
</div>
</u-row>
<div class="btn" @click="show=false">关闭</div>
</div>
</u-popup>
<u-modal v-model="modal" :content="'是否确定' + content + '该公告?'" title="" show-confirm-button
show-cancel-button confirm-text="确定" cancel-text="取消"
@confirm="confirm"></u-modal>
</div>
</template>
<script>
import AiAdd from "../../components/AiAdd";
import AiEmpty from "../../components/AiEmpty/AiEmpty";
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "notification",
components: {AiAdd, AiEmpty,AiTopFixed},
data() {
return {
index: 0,
show: false,
modal: false,
content: "",
current: 1,
dataList: [],
detail: {},
status: "加载更多",
}
},
onLoad(){
this.$dict.load("announcementStatus");
},
computed: {
tabs() {
return [{name: "最新公告"},{name: "公告管理"}];
},
optList(){
return [
{
name: "详情",
icon: this.$cdn + "notice/yl.png",
val: 0,
show: true,
},
{
name: "撤回",
icon: this.$cdn + "notice/ch.png",
val: 1,
show: this.detail?.status == 1,
},
{
name: "发布",
icon: this.$cdn + "notice/fb.png",
val: 2,
show: this.detail?.status == 0,
},
{
name: "编辑",
icon: this.$cdn + "notice/bj.png",
val: 3,
show: this.detail?.status == 0 || this.detail?.status == 3,
}, {
name: "删除",
icon: this.$cdn + "notice/sc.png",
val: 4,
show: true,
}
].filter(e=>e.show)
}
},
methods: {
changeState(){
this.$http.post(this.content =='删除' ? '/app/appannouncement/delete' : "/app/appannouncement/update-status",null,{
params: {
[this.content =='删除' ? 'ids' : 'id']:this.detail.id
}
}).then(res=>{
if(res.code==0){
this.$u.toast(this.content + "成功");
this.getList();
}
})
},
confirm(){
this.show = false;
this.changeState();
},
handleOpt(item){
this.content = {
1: "撤回",
2: "发布",
4: "删除",
}[item.val];
if (item.val == 0) {
this.show = false;
return uni.navigateTo({
url: "/pages/notification/components/detail?id=" + this.detail.id
});
}
if ([1,2,4].includes(item.val)) {
return this.modal = true;
}
if(item.val==3){
this.show = false;
return uni.navigateTo({
url:"/pages/notification/components/add?id=" + this.detail.id + "&flag=" + false
});
}
},
color(status){
return [
{backgroundColor:"rgba(255,136,34,0.1)",color:"#FF8822"},
{backgroundColor:"rgba(34,102,255,0.1)",color:"#2266FF"},
{backgroundColor:"rgba(102,102,102,0.1)",color:"#666666"},
{backgroundColor:"rgba(255,136,34,0.1)",color:"#FF8822"}
][status];
},
handeClick(item) {
this.detail = item;
if (this.index == 1) {
this.show = true;
}else {
uni.navigateTo({
url: "/pages/notification/components/detail?id=" + this.detail.id + "&flag=" + true
})
}
},
add(){
uni.navigateTo({
url: "/pages/notification/components/add"
})
},
change(val) {
this.index = val;
this.current = 1;
this.getList()
},
getList() {
this.$http.post(this.index ==0 ? "/app/appannouncement/list-latest" : "/app/appannouncement/list-mgr", null, {
params: {
size: 10,
current: this.current
}
}).then(res => {
if (res && res.data) {
if (this.current > 1 && this.current > res.data.pages) {
this.status = "已经到底啦"
}
this.dataList = this.current > 1 ? [...this.dataList, ...res.data.records] : res.data.records
}
})
}
},
onShow() {
this.current = 1;
this.getList();
},
onReachBottom() {
this.current = this.current + 1;
this.getList()
},
}
</script>
<style lang="scss" scoped>
.notification {
min-height: 100%;
background-color: #F5F5F5;
padding-bottom: 32px;
::v-deep .content{
padding: 0 !important;
}
.body {
box-sizing: border-box;
padding: 32px;
.card {
height: 208px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
padding: 32px;
border-radius: 8px;
background-color: #ffffff;
margin-bottom: 32px;
&:last-child{
margin-bottom: 0;
}
& > label {
font-size: 32px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
color: #333333;
line-height: 44px;
}
b {
display: inline-block;
font-style: normal;
width: 16px;
height: 16px;
border-radius: 50%;
background: #FF4466;
margin-right: 8px;
}
.tag{
width: 96px;
height: 44px;
display: inline-block;
border-radius: 8px;
margin-right: 16px;
font-size: 26px;
font-weight: 400;
line-height: 46px;
text-align: center;
}
.info {
font-size: 28px;
color: #999999;
line-height: 40px;
& > text:first-child {
margin-right: 32px;
}
}
& > .has-pic {
display: flex;
justify-content: space-between;
& > .left {
display: flex;
flex-direction: column;
justify-content: space-between;
& > label {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
& > img{
width: 192px;
height: 144px;
flex-shrink: 0;
margin-left: 32px;
}
}
}
}
.popup-wrap {
height: 368px;
background-color: #F7F7F7;
.btn{
height: 96px;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
font-weight: 500;
color: #333333;
background-color: #ffffff;
}
& > .u-row {
height: 272px;
box-sizing: border-box;
padding: 0 46px;
& > .colum {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 26px;
color: #666666;
}
}
}
}
</style>

View File

@@ -0,0 +1,326 @@
<template>
<section class="quickReply">
<ai-top-fixed background="#F5F5F5">
<u-tabs slot="tabs" :list="tabs" :is-scroll="false" :current="currentType" font-size="32"
bar-width="192" height="96" @change="handleTabClick"/>
<u-search placeholder="搜索" shape="square" bg-color="#fff" :show-action="false" search-icon-color="#ccc"
v-model="search.title" @search="page.current=1,getList()"/>
<ai-tabs v-model="search.category" :ops="categories" @change="getList" wrap>
<u-icon v-if="isPersonal" label="管理" size="38" class="manage" name="iconEdit" custom-prefix="iconfont"
color="#1365DD" slot="end"
label-color="#1365DD" @tap="handleManageType"/>
</ai-tabs>
</ai-top-fixed>
<div class="mainPanel">
<div class="fill">
<ai-card :card-title="e.title" v-for="(e,i) in list" :key="i" @send="handleSendMsg(e)">
<div class="textContent" v-if="e.contentType=='text'" v-html="e.content"/>
<div class="fileContent" v-else-if="e.contentType=='news'">
<ai-image link/>
<div class="info">
<span>{{ e.accessTitle }}</span>
<em>{{ e.accessUrl }}</em>
</div>
</div>
<div class="fileContent" v-else-if="e.contentType=='miniprogram'">
<ai-image miniapp/>
<div class="info">
{{ e.accessTitle }}
</div>
</div>
<div class="fileContent" v-else>
<template v-if="e.file">
<ai-image :src="e.file.url"/>
<div class="info">
<span>{{ e.file.name }}</span>
<em>{{ e.file.fileSizeStr }}</em>
</div>
</template>
</div>
<template #title>
<u-row>
<div class="top" v-if="e.isTop==1">置顶</div>
<div class="fill">{{ e.title }}</div>
</u-row>
</template>
<template v-if="isPersonal" #menu>
<div class="menu" @tap="handleTop(e)">{{ e.isTop == 1 ? '取消' : '' }}置顶</div>
<div class="menu" @tap="handleDetail(e.id)">编辑</div>
<div class="menu" @tap="handleDel(e.id)">删除</div>
</template>
</ai-card>
</div>
</div>
<div v-if="isPersonal" class="iconfont iconfont-iconAdd addBtn" @tap="handleDetail('')"/>
</section>
</template>
<script>
import AiTabs from "../../components/AiTabs";
import AiCard from "../../components/AiCard";
import {mapActions, mapState} from "vuex";
import AiImage from "../../components/AiImage";
import USticky from "../../uview/components/u-sticky/u-sticky";
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "quickReply",
components: {AiTopFixed, USticky, AiImage, AiCard, AiTabs},
computed: {
...mapState(['agentConfig']),
tabs() {
return [
{name: "个人话术", value: 0},
{name: "公共话术", value: 1},
]
},
categories() {
let meta = [{name: "全部分类", value: ""}]
this.originCategories.map(e => {
if (e.type == this.currentType) {
meta.push({name: e.name, value: e.id})
}
})
return meta
},
isPersonal() {
return this.currentType == 0
}
},
data() {
return {
currentType: 0,
page: {current: 1, size: 10, total: 0},
list: [],
originCategories: [],
search: {title: null, category: ''}
}
},
methods: {
...mapActions(['injectJWeixin', 'wxInvoke']),
handleManageType() {
uni.navigateTo({url: `./typeManage?type=${this.currentType}`})
},
handleDetail(id) {
uni.navigateTo({url: `./replyDetail?type=${this.currentType}&id=${id}`})
},
handleDel(id) {
this.$confirm("是否要删除该话术模板").then(() => {
this.$http.post("/app/wxcp/wxspeechtechnique/delete", null, {
params: {id}
}).then(res => {
if (res?.code == 0) {
this.$u.toast("删除成功!")
this.getList()
}
})
})
},
getList() {
this.$http.post("/app/wxcp/wxspeechtechnique/list", null, {
params: {...this.page, ...this.search, type: this.currentType}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
getCategories() {
this.$http.post("/app/wxcp/wxspeechtechniquecategory/listAll?type=-1").then(res => {
if (res?.data) {
this.originCategories = res.data
}
})
},
handleTabClick(i) {
this.page.current = 1
this.search = {}
this.currentType = i
this.getList()
},
handleSendMsg(e) {
let params = {
text: {
content: e.content || ""
},
image: {
mediaid: e.mediaId || "" //图片的素材id
},
video: {
mediaid: e.mediaId || "" //视频的素材id
},
file: {
mediaid: e.mediaId || "" //文件的素材id
},
news: {
link: e.accessUrl, //H5消息页面url 必填
title: e.accessTitle, //H5消息标题
desc: e.accessUrl,
imgUrl: this.$cdn + 'file.png', //H5消息封面图片URL
},
miniprogram: {
appid: e.accessAppid,//小程序的appid
title: e.accessTitle, //小程序消息的title
imgUrl: this.$cdn + 'miniwxmp.jpg',
page: e.accessUrl, //小程序消息打开后的路径,注意要以.html作为后缀否则在微信端打开会提示找不到页面
}
}
this.wxInvoke(['sendChatMessage', {
msgtype: e.contentType,
[e.contentType]: params[e.contentType]
}, res => {
if (res?.err_msg == 'sendChatMessage:ok') {
// this.$u.toast("发送成功!")
} else {
this.$u.toast("发送失败!")
}
}])
},
handleTop(row) {
let {isTop, id} = row
isTop = (Number(isTop) + 1) % 2
this.$http.post("/app/wxcp/wxspeechtechnique/setIsTop", null, {
params: {isTop, id}
}).then(res => {
if (res?.code == 0) {
this.$u.toast("设置成功!")
this.getList()
}
})
}
},
created() {
this.injectJWeixin("sendChatMessage")
this.getList()
this.getCategories()
},
onReachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
}
}
</script>
<style lang="scss" scoped>
.quickReply {
min-height: 100%;
background: #F5F5F5;
display: flex;
flex-direction: column;
.topFixed {
background: transparent;
}
::v-deep .u-tabs {
.u-scroll-box {
border-bottom: 1px solid #D4D4D4;
.u-tab-bar {
position: absolute;
bottom: -6px;
}
}
}
::v-deep .u-row {
flex-shrink: 0;
flex-wrap: nowrap;
align-items: center;
.manage {
flex-shrink: 0;
margin-bottom: 16px;
}
.top {
width: 84px;
height: 48px;
background: #F7F7F7;
border-radius: 4px;
color: #999;
font-size: 26px;
display: flex;
justify-content: center;
align-items: center;
}
}
.mainPanel {
padding: 40px 32px 110px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
& > * + * {
margin-top: 32px;
}
.menu {
text-align: center;
line-height: 80px;
width: 192px;
height: 80px;
font-size: 28px;
font-weight: 400;
color: #333333;
}
.AiCard {
margin-bottom: 32px;
}
}
.addBtn {
position: fixed;
right: 32px;
bottom: 64px;
color: $uni-color-primary;
font-size: 96px;
text-align: center;
font-weight: lighter;
}
.textContent {
overflow: hidden;
text-overflow: ellipsis;
}
.fileContent {
display: flex;
font-size: 32px;
align-items: center;
.info {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
min-width: 0;
& > * {
width: 100%;
word-break: break-all;
}
}
.AiImage {
margin-right: 20px;
}
em {
font-style: inherit;
font-size: 26px;
color: #9b9b9b;
}
}
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<section class="replyDetail">
<u-form :model="form" ref="replyForm" :rules="rules">
<u-form-item label="分类" prop="category" required>
<ai-select :value="form.category" :list="categories" @data="v=>form.category=v[0].value"/>
</u-form-item>
<u-form-item label="标题">
<u-input class="textRight" v-model="form.title" placeholder="选填,仅内部可见" maxlength="30"/>
</u-form-item>
<u-form-item label="快捷回复类型" label-position="top" prop="contentType" required>
<ai-tabs wrap plain :ops="types" v-model="form.contentType" @change="$refs.replyForm.setRules(rules)"/>
</u-form-item>
<u-form-item v-if="form.contentType=='text'" label="内容" label-position="top" prop="content" required>
<ai-textarea v-model="form.content" maxlength="1000" placeholder="限1000字">
<template #bar>
<div class="textareaBtn" @tap="handleInsert(custom.name)">插入用户昵称</div>
<!-- <div class="textareaBtn" @tap="handleInsert(custom.name)">插入备注名</div>-->
</template>
</ai-textarea>
</u-form-item>
<u-form-item v-else-if="['image','video','file','voice'].includes(form.contentType)"
:label="currentContentType.name"
label-position="top" prop="mediaId" required>
<ai-uploader :type="form.contentType" :mediaId.sync="form.mediaId" :fileId.sync="form.fileId"
:placeholder="`添加${currentContentType.name}`" :def="form.file"/>
</u-form-item>
<template v-else-if="['news'].includes(form.contentType)">
<u-form-item label="链接地址" label-position="top" prop="accessUrl" required>
<u-input v-model="form.accessUrl" placeholder="链接地址请以http或https开头"/>
</u-form-item>
<u-form-item label="标题" prop="accessTitle" required>
<u-input class="textRight" v-model="form.accessTitle" maxlength="50" placeholder="请输入链接标题"/>
</u-form-item>
</template>
<template v-else-if="['miniprogram'].includes(form.contentType)">
<u-form-item label="AppId" prop="accessAppid" required>
<u-input class="textRight" v-model="form.accessAppid" placeholder="请输入小程序Appid"/>
</u-form-item>
<u-form-item label="页面路径" prop="accessUrl" required label-width="140">
<u-input class="textRight" v-model="form.accessUrl" placeholder="请输入小程序页面路径"/>
</u-form-item>
<u-form-item label="标题" prop="accessTitle" required>
<u-input class="textRight" v-model="form.accessTitle" maxlength="50" placeholder="请输入链接标题"/>
</u-form-item>
</template>
</u-form>
<div bottom>
<u-button @tap="back">取消</u-button>
<u-button type="primary" @tap="submitReply">保存</u-button>
</div>
</section>
</template>
<script>
import AiSelect from "../../components/AiSelect";
import AiTabs from "../../components/AiTabs";
import AiTextarea from "../../components/AiTextarea";
import AiUploader from "../../components/AiUploader";
import {mapActions} from "vuex";
export default {
name: "replyDetail",
components: {AiUploader, AiTextarea, AiTabs, AiSelect},
computed: {
types() {
return [
{name: "文字", value: "text"},
{name: "图片", value: "image"},
{name: "视频", value: "video"},
{name: "附件", value: "file"},
{name: "链接", value: "news"},
{name: "小程序", value: "miniprogram"},
]
},
currentContentType() {
return this.types.find(e => e.value == this.form.contentType) || {}
},
rules() {
return {
category: [{required: true, message: "请选择分类"}],
contentType: [{required: true, message: "请选择快捷回复类型"}],
content: [{required: true, message: "请填写内容"}],
accessUrl: [{required: true, message: "请填写链接地址"}],
accessAppid: [{required: true, message: "请填写小程序AppId"}],
accessTitle: [{required: true, message: "请填写标题"}],
mediaId: [{required: true, message: `请上传${this.currentContentType.name}`}],
}
}
},
data() {
return {
form: {contentType: "text"},
categories: [],
type: "",
custom: {}
}
},
methods: {
...mapActions(['injectJWeixin', 'wxInvoke']),
back() {
uni.redirectTo({url: "./quickReply"})
},
getCategories() {
this.$http.post("/app/wxcp/wxspeechtechniquecategory/listAll", null, {
params: {type: this.type}
}).then(res => {
if (res?.data) {
this.categories = res.data.map(e => ({label: e.name, value: e.id}))
}
})
},
getDetail(id) {
this.$http.post("/app/wxcp/wxspeechtechnique/list", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.form = res.data.records?.[0]
}
})
},
submitReply() {
this.$refs.replyForm.validate(v => {
if (v) {
this.$http.post("/app/wxcp/wxspeechtechnique/addOrUpdate", {...this.form, type: this.type}).then(res => {
if (res?.code == 0) {
this.$u.toast("提交成功!")
uni.navigateTo({url: './quickReply'})
}
})
}
})
},
getCustom() {
this.wxInvoke(['getCurExternalContact', {}, res => {
if (res?.err_msg == 'getCurExternalContact:ok') {
this.$http.post("/app/wxcp/wxcustomer/queryCustomerById", null, {
params: {id: res.userId}
}).then(ret => {
if (ret?.data) {
this.custom = ret.data
}
})
}
}])
},
handleInsert(str) {
if (str) {
this.form.content = (this.form.content || "") + str
this.$forceUpdate()
}
}
},
created() {
this.injectJWeixin("getCurExternalContact").then(() => this.getCustom())
let {type, id} = this.$route.query
this.type = type
this.getCategories()
id && this.getDetail(id)
},
mounted() {
this.$nextTick(() => this.$refs.replyForm.setRules(this.rules))
}
}
</script>
<style lang="scss" scoped>
.replyDetail {
border-top: 1px solid #D4D4D4;
padding: 16px 0 208px;
background: #F5F5F5;
box-sizing: border-box;
::v-deep .u-form-item {
.u-form-item--left {
margin-bottom: 0 !important;
}
.u-form-item--right__content__slot {
justify-content: flex-end;
}
& + .u-form-item {
margin-top: 16px;
}
.textRight {
.uni-input-wrapper {
text-align: right;
}
}
}
.textareaBtn {
font-size: 26px;
color: #1365DD;
}
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<section class="typeManage">
<v-drag v-model="list" @move="handleMove">
<div class="item" v-for="(e,i) in list" :key="i">
<u-icon name="list"/>
<u-input v-model="e.name" border :focus="!e.id" @blur="submitCategory(e,i)" maxlength="6"/>
<div btn @tap="handleDel(e,i)">删除</div>
</div>
</v-drag>
<div v-if="!adding" class="item" @tap="handleAdd">
<u-icon name="plus-circle" size="38" color="#1365DD"/>
<div btn>添加新的分组</div>
</div>
<div bottom>
<u-button @tap="back">返回</u-button>
</div>
</section>
</template>
<script>
import VDrag from "../../components/VDrag";
export default {
name: "typeManage",
components: {VDrag},
data() {
return {
list: [],
adding: false,
moveIndex: 0
}
},
methods: {
back() {
uni.redirectTo({url: "./quickReply"})
},
getList() {
this.$http.post("/app/wxcp/wxspeechtechniquecategory/listAll", null, {
params: {type: this.$route.query?.type}
}).then(res => {
if (res?.data) {
this.list = res.data.map(e => ({...e, disabled: true})).sort((a, b) => a.showIndex - b.showIndex)
}
})
},
handleAdd() {
this.adding = true
this.list.push({
showIndex: this.list.length,
type: this.$route.query?.type
})
},
handleDel(item, index) {
this.$confirm("是否要删除该分类?").then(() => {
if (item?.id) {
this.$http.post("/app/wxcp/wxspeechtechniquecategory/delete", null, {
params: {id: item.id}
}).then(() => {
this.$u.toast("删除成功!")
this.getList()
})
} else this.list.splice(index, 1)
})
},
submitCategory(item, index) {
this.adding = false
if (item?.name) {
this.$http.post("/app/wxcp/wxspeechtechniquecategory/addOrUpdate", item).then(() => {
this.getList()
})
} else {
this.list.splice(index, 1)
}
},
handleMove({moved}) {
if (moved) {
let {oldIndex, newIndex} = moved,
list = JSON.parse(JSON.stringify(this.list)),
item = list?.[oldIndex]
if (oldIndex < newIndex) {
list.splice(newIndex + 1, 0, item)
list.splice(oldIndex, 1)
} else {
list.splice(oldIndex, 1)
list.splice(newIndex, 0, item)
}
this.$http.post("/app/wxcp/wxspeechtechniquecategory/updateSort",
list.map((e, showIndex) => ({id: e.id, showIndex}))
).then(res => {
if (res?.code == 0) {
this.getList()
}
})
}
}
},
created() {
this.getList()
},
}
</script>
<style lang="scss" scoped>
.typeManage {
padding: 32px 0 80px;
border-top: 1px solid #D4D4D4;
display: flex;
flex-direction: column;
& > * + * {
margin-top: 32px;
}
uni-movable-area {
width: 100%;
height: auto;
}
.item {
display: flex;
height: 88px;
width: 100%;
align-items: center;
position: relative;
& + .item {
margin-top: 32px;
}
&:hover {
z-index: 9;
}
}
.iconfont, .u-icon {
font-size: 38px;
padding: 0 32px;
color: #89b;
& + div[btn] {
padding: 0;
}
}
div[btn] {
color: #1365DD;
padding: 0 26px;
cursor: pointer;
}
::v-deep .u-input {
background: #FBFBFB;
border-radius: 4px;
border: 1px solid #CCCCCC;
height: 100%;
uni-input {
height: 100%;
}
}
}
</style>

100
src/pages/resident/comp.vue Normal file
View File

@@ -0,0 +1,100 @@
<template>
<section class="comp">
<ai-result v-if="error" status="error" :tips="error"/>
<template v-else-if="isNormal">
<component ref="currentTab" :is="currentTab.comp" :isNormal="isNormal"/>
<ai-tabbar :active.sync="active" :list="bottomBar"/>
</template>
<ai-loading v-else tips="居民管理加载中..."/>
</section>
</template>
<script>
import {mapActions} from "vuex";
import AiLoading from "../../components/AiLoading";
import AiResult from "../../components/AiResult";
import ResidentSta from "./residentSta";
import ResidentList from "./residentList";
import GroupList from "./groupList";
import AiTabbar from "../../components/AiTabbar";
export default {
name: "comp",
components: {AiTabbar, GroupList, ResidentList, ResidentSta, AiResult, AiLoading},
data() {
return {
error: "",
active: 0,
isNormal: false
}
},
computed: {
bottomBar() {
const link = icon => `${this.$cdn}resident/${icon}.png`
return [
{text: "统计分析", iconPath: "qwhome2", selectedIconPath: "qwhome1", comp: ResidentSta},
{text: "居民列表", iconPath: "qwjmda1", selectedIconPath: "qwjmda2", comp: ResidentList},
{text: "居民群列表", iconPath: "qwjmq1", selectedIconPath: "qwjmq2", comp: GroupList},
].map(e => ({
...e,
iconPath: link(e.iconPath),
selectedIconPath: link(e.selectedIconPath)
}))
},
currentTab() {
return this.bottomBar?.[this.active] || {}
}
},
methods: {
...mapActions(['injectJWeixin', "wxInvoke"]),
},
created() {
if (this.$route.hash == "#dev") {
this.isNormal = true
} else this.injectJWeixin(["getContext", "getCurExternalChat"]).then(() => {
this.wxInvoke(['getContext', {}, res => {
if (res.err_msg == "getContext:ok") {
if (res.entry == 'normal') this.isNormal = true
else this.wxInvoke(['getCurExternalChat', {}, res => {
if (res?.err_msg == 'getCurExternalChat:ok') {
wx.redirectTo({
url: "./groupResident"
})
} else {
wx.redirectTo({
url: "./resident"
})
}
}])
} else {
this.error = "wxwork:获取进入场景失败"
}
}])
}).catch(() => {
this.error = "应用加载失败"
})
},
onReachBottom() {
if (typeof this.$refs?.currentTab?.reachBottom == 'function') this.$refs?.currentTab.reachBottom()
}
}
</script>
<style lang="scss" scoped>
.comp {
height: 100%;
::v-deep .u-tabbar__content__item {
padding: 0;
.u-icon__img {
height: 44px !important;
width: 44px !important;
}
.u-tabbar__content__item__text {
margin-top: 4px;
font-size: 22px;
}
}
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<section class="document">
<div class="card">
<div class="info">
<u-image border-radius="4" :src="top.detail.avatar" width="118" height="118"/>
<div class="fill">
<b>{{ top.detail.realName || top.detail.name }}</b>
<u-row>
<span class="idNumber" v-html="IDObj.id"/>
<a @tap="showID=!showID">{{ IDObj.btn }}</a>
</u-row>
</div>
</div>
<ai-cell label="性别">{{ $dict.getLabel("sex", resident.sex) || "-" }}</ai-cell>
<ai-cell label="出生日期">{{ resident.birthDate }}</ai-cell>
<ai-cell label="年龄">{{ resident.age }}</ai-cell>
<ai-cell label="籍贯">{{ resident.birthplaceAreaName }}</ai-cell>
<ai-cell label="民族">{{ $dict.getLabel("nation", resident.nation) || "-" }}</ai-cell>
<ai-cell label="文化程度">{{ $dict.getLabel("education", resident.education) || "-" }}</ai-cell>
<ai-cell label="兵役状况">{{ $dict.getLabel("militaryStatus", resident.militaryStatus) || "-" }}</ai-cell>
<ai-cell label="政治面貌">{{ $dict.getLabel("politicsStatus", resident.politicsStatus) || "-" }}</ai-cell>
<ai-cell label="职业">{{ $dict.getLabel("job", resident.job) || "-" }}</ai-cell>
<ai-cell label="宗教信仰">{{ $dict.getLabel("faithType", resident.faithType) || "-" }}</ai-cell>
</div>
<div class="card">
<ai-cell title label="联络信息"/>
<ai-cell label="联系方式">{{ resident.phone }}</ai-cell>
<ai-cell label="现住址">{{ resident.currentAreaName + resident.currentAddress }}</ai-cell>
</div>
<div class="card">
<ai-cell title label="家庭信息"/>
<ai-cell label="是否户主">{{ $dict.getLabel("householdName", resident.householdName) || "-" }}</ai-cell>
<ai-cell label="与户主关系">{{ $dict.getLabel("householdRelation", resident.householdRelation) || "-" }}</ai-cell>
<ai-cell label="现住址">{{ resident.householdAreaName + resident.householdAddress }}</ai-cell>
</div>
<div class="card">
<ai-cell title label="家庭成员"/>
<ai-table :data="family" :colConfigs="colConfigs"/>
</div>
</section>
</template>
<script>
import AiCell from "../../components/AiCell";
import AiTable from "../../components/AiTable";
export default {
name: "document",
components: {AiTable, AiCell},
inject: ['top'],
computed: {
IDObj() {
return this.showID ? {
id: this.resident?.idNumber,
btn: '隐藏'
} : {
id: this.resident?.idNumber?.replace(/(\d{10}).+/g, '$1******'),
btn: '显示'
}
},
colConfigs() {
return [
{label: "与户主关系", prop: "householdRelation", width: '160rpx', dict: "householdRelation"},
{label: "姓名", prop: "name", width: '120rpx'},
{label: "性别", prop: "sex", dict: "sex"},
{label: "年龄", prop: "age"},
{label: "身份证号", prop: "idNumber", width: '320rpx'},
]
},
resident() {
let obj = {}
Object.keys(this.top.detail?.residentInfo?.resident || {}).map(e => {
obj[e] = this.top.detail?.residentInfo?.resident[e] || ""
})
return obj
},
family() {
return this.top.detail?.residentInfo?.family?.map(e => ({...e, householdRelation: e.householdRelation || "户主"}))
}
},
data() {
return {
showID: false,
familyList: []
}
},
created() {
this.$dict.load("sex", "nation", "education", "job",
"faithType", "politicsStatus", "militaryStatus", "householdRelation",
"householdName")
}
}
</script>
<style lang="scss" scoped>
.document {
overflow-y: auto;
background: #F5F5F5;
.info {
height: 186px;
width: 100%;
display: flex;
align-items: center;
.fill {
color: #3C7FC8;
margin-left: 24px;
font-size: 28px;
line-height: 40px;
display: flex;
flex-direction: column;
b {
font-size: 36px;
color: #333;
margin-bottom: 8px;
line-height: 50px;
}
a {
cursor: pointer;
}
.idNumber {
margin-right: 16px;
color: #999;
}
}
}
}
</style>

View File

@@ -0,0 +1,182 @@
<template>
<section class="groupList">
<ai-top-fixed>
<u-search placeholder="请输入群名、群主名" :show-action="false" search-icon-color="#ccc"
v-model="search.name" @search="page.current=1,getList()"/>
<ai-cell>
<b slot="label" class="title"><i v-html="page.total||0"/>个居民群</b>
</ai-cell>
</ai-top-fixed>
<div class="mainPane">
<ai-cell v-for="item in list" :key="item.id" @click.native="showResident(item)">
<template #label>
<ai-image :src="item.avatar" preview/>
</template>
<div class="card column start" flex>
<div flex class="groupName">
<b>{{ item.name || "群聊" }}</b>
<div class="personCount" v-if="item.personCount">({{ item.personCount }})</div>
</div>
<div class="owner" v-html="`群主:${item.ownerName}`"/>
<div flex class="trends">
<div flex v-html="`今日入群:<em>${item.increase||0}</em>`"/>
<div flex v-html="`今日退群:<p>${item.decrease||0}</p>`"/>
</div>
</div>
</ai-cell>
</div>
</section>
</template>
<script>
import AiCell from "../../components/AiCell";
import AiImage from "../../components/AiImage";
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "groupList",
components: {AiTopFixed, AiImage, AiCell},
data() {
return {
page: {current: 1, size: 10, total: 0},
search: {name: ""},
list: []
}
},
methods: {
getList() {
this.$http.post("/app/wxcp/wxgroup/list", null, {
params: {...this.page, ...this.search}
}).then(res => {
if (res?.data) {
let meta = res.data.records?.map(e => ({
...e,
avatar: e?.avatar || this.$cdn + "groupAvatar.png"
}))
if (this.page.current > 1) {
this.list = [...this.list, ...meta]
} else this.list = meta
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
showResident({id}) {
id && uni.navigateTo({
url: './groupResident?id=' + id
})
}
},
created() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.groupList {
::v-deep .AiTopFixed {
b.title {
color: #333;
font-size: 32px;
& > i {
color: #267FCE;
font-style: normal;
margin: 0 4px;
}
}
}
::v-deep .mainPane {
background: #fff;
padding: 0 32px;
.AiCell {
align-items: center;
height: 230px;
justify-content: flex-start;
.content {
flex: 1;
min-width: 0;
height: 100%;
max-width: unset;
border-bottom: 1px solid rgba(221, 221, 221, 1);
}
}
.card {
height: 100%;
justify-content: center;
b {
font-size: 36px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tag {
justify-content: center;
background: #F3F4F7;
border-radius: 4px;
padding: 0 16px;
font-size: 28px;
font-weight: 400;
color: #333;
margin-left: 16px;
height: 56px;
}
.groupName {
width: 100%;
text-align: left;
}
.owner, .trends {
margin-top: 8px;
font-size: 28px;
font-weight: 400;
color: #999;
}
.personCount {
flex-shrink: 0;
font-size: 30px;
font-weight: 400;
color: #666;
}
.trends {
* + * {
margin-left: 24px;
}
em {
font-style: normal;
color: #5FBA95;
}
p {
color: #F09535;
}
}
}
.AiImage {
margin-right: 24px;
image {
width: 112px;
height: 112px;
}
}
}
}
</style>

View File

@@ -0,0 +1,305 @@
<template>
<div class="group-resident">
<template v-if="!showTagManage">
<ai-top-fixed>
<div class="card">
<header>
<u-avatar mode="square" :src="$cdn + 'groupAvatar.png'" :size="112"></u-avatar>
{{ detail.name }}
</header>
<u-grid :col="2" :border="false">
<u-grid-item>
<label>建群日期</label>
<label v-if="detail.createTime">{{ detail.createTime.split(" ")[0] }}</label>
</u-grid-item>
<u-grid-item>
<label>成员总数</label>
<label v-if="isToday">{{ detail.statistic.today.total }}</label>
</u-grid-item>
<u-grid-item>
<label>今日入群</label>
<label v-if="isToday">{{ detail.statistic.today.increase }}</label>
</u-grid-item>
<u-grid-item>
<label>今日退群</label>
<label v-if="isToday">{{ detail.statistic.today.decrease }}</label>
</u-grid-item>
<!-- <u-grid-item>-->
<!-- <label>今日活跃人数</label>-->
<!-- <label>{{item.value}}</label>-->
<!-- </u-grid-item>-->
</u-grid>
<p class="statistics">
<span>成员性别</span>
<label>男性</label>
<b>{{ detail.man }}</b>
<label>女性</label>
<b>{{ detail.woman }}</b>
<label>未知</label>
<b>{{ detail.unknown }}</b>
</p>
</div>
</ai-top-fixed>
<div class="card">
<ai-cell title label="群标签">
<u-icon label="添加" size="38" name="iconAdd" custom-prefix="iconfont" color="#1365DD"
label-color="#1365DD" @tap="showTagManage=true"/>
</ai-cell>
<ai-cell top-label v-for="(op,name) in tagsList" :label="name" :key="name">
<u-row>
<div class="tag" v-for="(tag,j) in op" :key="j">{{ tag }}</div>
</u-row>
</ai-cell>
</div>
<div class="card">
<ai-cell title label="群成员"></ai-cell>
<div class="wrap">
<div class="item" v-for="(item,index) in detail.groupUserList" :key="index" @click="handleWechat(item)">
<u-avatar mode="square" :src="item.avatar"></u-avatar>
<div class="info">
<div class="left">
<p>{{ item.memberName }}
<b v-if="item.customerType==2" style="color: #3C7FC8;">@{{ item.corpName }}</b>
<b v-if="item.customerType==1" style="color: #2EA222;">@微信</b>
</p>
<p>真实姓名{{ item.realName }}</p>
</div>
<span v-if="item.userId==detail.owner">群主</span>
</div>
</div>
</div>
</div>
</template>
<tag-manage v-if="showTagManage"/>
<ai-back v-if="isNormal&&!showTagManage"/>
</div>
</template>
<script>
import AiCell from "../../components/AiCell";
import {mapActions} from "vuex";
import TagManage from "./tagManage";
import AiBack from "../../components/AiBack";
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "groupResident",
components: {AiTopFixed, AiBack, AiCell, TagManage},
provide() {
return {
top: this
}
},
data() {
return {
detail: {},
groupId: null,
showTagManage: false,
}
},
computed: {
tagsList() {
let obj = {}
this.detail?.tagList?.map(e => {
if (e?.groupName) {
if (obj?.[e.groupName]) {
obj[e.groupName].push(e.tagName)
} else {
obj[e.groupName] = [e.tagName]
}
}
})
return obj
},
isToday() {
return !!this.detail?.statistic?.today
},
isNormal() {
return !!this.$route.query.id;
}
},
methods: {
...mapActions(['injectJWeixin', "wxInvoke"]),
getContact() {
if (this.isNormal) {
this.groupId = this.$route.query.id
this.getGroup(this.$route.query.id)
} else this.injectJWeixin("getCurExternalChat").then(() => {
this.wxInvoke(['getCurExternalChat', {}, res => {
if (res?.err_msg == 'getCurExternalChat:ok') {
this.groupId = res.chatId
this.getGroup(res.chatId)
}
}])
})
},
getGroup(id) {
this.$http.post("/app/wxcp/wxgroup/getDetail", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.detail = res.data
}
})
},
handleWechat({userId,type}) {
this.injectJWeixin('openUserProfile').then(()=>{
this.wxInvoke(['openUserProfile', {
type,
userid: userId
}, () => 0])
})
}
},
created() {
this.getContact()
}
}
</script>
<style lang="scss" scoped>
.group-resident {
width: 100%;
min-height: 100%;
background-color: #F5F5F5;
display: flex;
flex-direction: column;
::v-deep .AiTopFixed {
& > .card {
margin-top: 0;
}
}
::v-deep .card {
background-color: #FFFFFF;
padding: 16px 32px;
box-sizing: border-box;
margin-top: 16px;
header {
height: 192px;
display: flex;
align-items: center;
font-size: 36px;
font-weight: 600;
color: #333333;
.u-avatar {
margin-right: 24px;
}
}
::v-deep .u-grid-item-box {
box-sizing: border-box;
padding: 16px !important;
.uni-label-pointer {
width: 100%;
line-height: 40px;
color: #999999;
}
uni-label:last-child {
margin-top: 16px;
font-weight: 600;
color: #333333;
}
}
.statistics {
height: 96px;
display: flex;
align-items: center;
border-top: 1px solid #eee;
margin-top: 16px;
span {
font-size: 28px;
color: #999999
}
label + b {
font-size: 28px;
color: #333333;
}
b {
color: #1365DD !important;
margin: 0 32px 0 16px;
}
}
& > section:first-child {
height: 90px !important;
display: flex;
align-items: center;
}
.wrap {
margin-bottom: 20px;
.item {
height: 176px;
display: flex;
align-items: center;
.info {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 24px;
border-bottom: 1px solid #eee;
.left {
font-size: 36px;
font-weight: 600;
color: #333333;
b {
font-size: 28px;
font-weight: 400;
color: #3C7FC8;
margin-left: 16px;
}
p:last-child {
font-size: 28px;
font-weight: 400;
color: #999999;
margin-top: 8px;
}
}
span {
width: 88px;
height: 56px;
text-align: center;
background: rgba(19, 101, 221, 0.1);
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
border-radius: 4px;
font-size: 28px;
font-weight: 400;
color: #1365DD;
line-height: 56px;
}
}
}
}
}
::v-deep .tag {
height: 56px;
line-height: 56px;
background: #F3F4F7;
border-radius: 6px;
padding: 8px 16px;
margin-right: 16px;
margin-bottom: 16px;
overflow: hidden;
cursor: default;
}
}
</style>

359
src/pages/resident/info.vue Normal file
View File

@@ -0,0 +1,359 @@
<template>
<section class="info">
<div class="card">
<div class="baseInfo">
<u-image border-radius="4" :src="top.detail.avatar" width="118" height="118"/>
<div class="fill">
<b>{{ top.detail.name }}</b>
<div v-if="top.detail.type==1" class="wx">@微信</div>
<div v-if="top.detail.type==2">@企业微信</div>
</div>
<div class="certBtn" @tap="handleCert">{{ !isCert ? "实名认证" : "解绑" }}</div>
</div>
<u-row>
<ai-cell class="half" top-label label="来源">{{ $dict.getLabel("wxCustomerAddWay", top.detail.addWay) }}</ai-cell>
<ai-cell class="half" top-label label="添加时间">{{ top.detail.createTime }}</ai-cell>
<ai-cell class="half" top-label label="真实姓名">{{ top.detail.realName }}</ai-cell>
<ai-cell class="half" top-label label="手机号码">{{ resident.phone || "-" }}</ai-cell>
<ai-cell class="half" top-label label="家庭积分">{{ resident.familyIntegral }}</ai-cell>
<ai-cell class="half" top-label label="个人积分">{{ resident.personalIntegral }}</ai-cell>
</u-row>
</div>
<div class="card">
<ai-cell title label="公共标签">
<u-icon label="添加" size="38" name="iconAdd" custom-prefix="iconfont" color="#1365DD"
label-color="#1365DD" @tap="top.showTagManage=true"/>
</ai-cell>
<ai-cell top-label v-for="(op,name) in tagsList" :label="name" :key="name">
<u-row>
<div class="tag" v-for="(tag,j) in op" :key="j">{{ tag }}</div>
</u-row>
</ai-cell>
</div>
<div class="card">
<ai-cell title label="动态"/>
<ai-cell top-label>
<div class="logItem" v-for="item in customLogs" :key="item.id">
<div flex class="column" shrink>
<div class="dot"/>
<div class="line fill"/>
</div>
<div flex class="start column">
<b>{{ $dict.getLabel('wxCustomerLogType', item.type) }}</b>
<span>{{ item.createTime }}</span>
<div v-html="item.content"/>
</div>
</div>
</ai-cell>
</div>
<u-mask :show="dialog" @tap="dialog=false">
<div class="bindCert" @tap.stop>
<b class="title">实名认证</b>
<u-input class="searchInput" v-model="search" clearable placeholder="请输入姓名或身份证号" @input="handleSearch"/>
<div class="residents">
<div flex class="spb" v-for="(op,i) in result" :key="i" @tap="bindCert(op.id)">
<div v-html="op.name"/>
<div v-html="op.idNumber"/>
</div>
</div>
</div>
</u-mask>
<div bottom>
<u-button type="primary" @tap="handleWechat">微信联系</u-button>
<u-button v-if="isMobile" :disabled="!resident.phone" @tap="handleTel">电话联系</u-button>
</div>
</section>
</template>
<script>
import AiCell from "../../components/AiCell";
import UMask from "../../uview/components/u-mask/u-mask";
import UInput from "../../uview/components/u-input/u-input";
import USteps from "../../uview/components/u-steps/u-steps";
import ULine from "../../uview/components/u-line/u-line";
import UButton from "../../uview/components/u-button/u-button";
import {mapActions} from "vuex";
export default {
name: "info",
components: {UButton, ULine, USteps, UInput, UMask, AiCell},
inject: ['top'],
computed: {
resident() {
return this.top.detail?.residentInfo?.resident || {}
},
tagsList() {
let obj = {}
this.top.detail?.tags?.map(e => {
if (e.type == 1 && e?.groupName) {
if (obj?.[e.groupName]) {
obj[e.groupName].push(e.tagName)
} else {
obj[e.groupName] = [e.tagName]
}
}
})
return obj
},
isCert() {
return !!this.top?.detail?.residentInfo
},
isMobile() {
return ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]
.some(e => navigator.userAgent.indexOf(e) > -1)
}
},
data() {
return {
dialog: false,
search: "",
result: [],
customLogs: []
}
},
methods: {
...mapActions(['injectJWeixin', 'wxInvoke']),
searchResident() {
this.$http.post("/app/appresident/check-name", null, {
params: {name: this.search}
}).then(res => {
if (res?.data) {
let reg = new RegExp(this.search, 'g')
this.result = res.data?.map(e => ({
...e,
name: e.name.replace(reg, `<b>${this.search}</b>`),
idNumber: e.idNumber.replace(reg, `<b>${this.search}</b>`),
}))
}
})
},
handleSearch() {
if (this.search?.length >= 2) {
this.searchResident()
} else {
this.result = []
}
},
handleCert() {
if (this.isCert) {
this.$confirm("是否要解绑当前实名认证?").then(() => {
this.$http.post("/app/wxcp/wxcustomer/unBindCustomer2Resident", null, {
params: {residentId: this.resident.id, customerId: this.top.custom}
}).then(res => {
if (res?.code == 0) {
this.$u.toast("解除绑定成功!")
this.top.getContact()
}
})
})
} else this.dialog = true
},
bindCert(residentId) {
this.dialog = false
this.$confirm("是否要绑定该居民?").then(() => {
this.$http.post("/app/wxcp/wxcustomer/bindCustomer2Resident", null, {
params: {residentId, customerId: this.top.custom}
}).then(res => {
if (res?.code == 0) {
this.$u.toast("绑定成功!")
this.top.getContact()
}
}).catch(err => {
this.$u.toast(err)
setTimeout(() => this.dialog = true, 1000)
})
}).catch(() => this.dialog = true)
},
getCustomLog(customerId) {
customerId && this.$http.post("/app/wxcp/wxcustomerlog/listAll", null, {
params: {customerId}
}).then(res => {
if (res?.data) {
this.customLogs = res.data
}
})
},
handleTel() {
location.href = "tel:" + this.resident.phone
},
handleWechat() {
this.wxInvoke(['openUserProfile', {
type: 2,
userid: this.top.custom
}, () => 0])
}
},
created() {
this.$dict.load("wxCustomerAddWay", 'wxCustomerLogType')
this.getCustomLog(this.top.custom)
this.injectJWeixin('openUserProfile')
}
}
</script>
<style lang="scss" scoped>
.info {
padding-bottom: 130px;
.certBtn {
cursor: pointer;
background: $uni-color-primary;
color: #fff;
padding: 8px 16px;
border-radius: 4px;
}
.baseInfo {
height: 186px;
width: 100%;
display: flex;
align-items: center;
.fill {
color: #3C7FC8;
margin-left: 24px;
font-size: 28px;
line-height: 40px;
b {
font-size: 36px;
color: #333;
margin-bottom: 8px;
line-height: 50px;
}
.wx {
color: #2EA222;
}
}
}
::v-deep .AiCell {
&.half {
width: 50%;
}
}
::v-deep .u-form-item {
width: 50%;
min-height: 124px;
.u-form-item--left {
color: #999;
min-height: 40px;
}
.u-form-item--right {
min-height: 0;
margin-top: 16px;
}
}
::v-deep .u-mask {
display: flex;
align-items: center;
justify-content: center;
.bindCert {
width: 600px;
padding: 32px;
min-height: 400px;
background-color: #fff;
display: flex;
flex-direction: column;
color: #333;
}
.residents {
max-height: 780px;
overflow-y: auto;
}
.title {
text-align: center;
font-size: 36px;
}
.searchInput {
margin: 16px 0;
border: 1px solid #D0D4DC;
border-radius: 8px;
padding: 0 16px !important;
flex: 0;
}
.spb {
height: 60px;
cursor: pointer;
padding: 0 16px;
b {
font-size: 32px;
}
&:nth-of-type(2n) {
background: #eee;
}
}
}
::v-deep .logItem {
display: flex;
min-height: 220px;
&:last-of-type {
.line {
display: none;
}
}
& > .column + .column {
margin-left: 16px;
}
.dot {
flex-shrink: 0;
width: 16px;
height: 16px;
background: $uni-color-primary;
border: 8px solid #FFFFFF;
border-radius: 50%;
margin: 8px 0;
}
.line {
width: 4px;
background: #eee;
}
.start {
font-size: 26px;
font-weight: 400;
color: #666;
b {
color: #333;
}
i {
color: $uni-color-primary;
font-style: normal;
}
& > b {
font-size: 32px;
font-weight: bold;
margin-bottom: 8px;
}
& > span {
color: #999;
}
& > div {
margin-top: 16px;
}
}
}
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<section class="resident">
<ai-loading v-if="!custom&&!error" tips="获取居民信息中..."/>
<ai-result v-else-if="error" status="error" :tips="error"/>
<template v-else>
<tag-manage v-if="showTagManage"/>
<template v-else>
<ai-top-fixed>
<u-tabs :list="tabs" :is-scroll="false" :current="currentType" font-size="32"
bar-width="192" height="96" @change="handleTabClick"/>
</ai-top-fixed>
<component :is="currentTab.comp"/>
</template>
</template>
<ai-back v-if="isNormal&&!showTagManage"/>
</section>
</template>
<script>
import {mapActions} from "vuex";
import Info from "./info";
import Document from "./document";
import TagManage from "./tagManage";
import AiLoading from "../../components/AiLoading";
import AiResult from "../../components/AiResult";
import AiBack from "../../components/AiBack";
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "resident",
components: {AiTopFixed, AiBack, AiResult, AiLoading, TagManage, Document, Info},
provide() {
return {
top: this
}
},
computed: {
tabs() {
return [
{name: "居民信息", value: 0, comp: Info},
{name: "居民档案", value: 1, comp: Document, hide: !this.detail.residentInfo},
].filter(e => !e.hide)
},
currentTab() {
return this.tabs.find(e => e.value == this.currentType)
},
isNormal() {
return !!this.$route.query.id;
}
},
data() {
return {
currentType: 0,
detail: {},
showTagManage: false,
custom: "",
error: ""
}
},
methods: {
...mapActions(['injectJWeixin', 'wxInvoke']),
handleTabClick(i) {
this.currentType = i
},
getContact() {
if (this.isNormal) {
this.getCustom(this.$route.query.id)
} else {
this.injectJWeixin("getCurExternalContact").then(() => {
this.wxInvoke(['getCurExternalContact', {}, res => {
if (res?.err_msg == 'getCurExternalContact:ok') {
this.getCustom(res.userId)
}
}])
}).catch(({errMsg}) => {
this.error = errMsg
})
}
},
getCustom(id) {
this.$http.post("/app/wxcp/wxcustomer/queryCustomerById", null, {
params: {id}
}).then(ret => {
if (ret?.data) {
this.custom = id
this.detail = ret.data
if (this.detail.type == 2) {
this.error = "只能查看个人微信绑定的居民信息"
}
}
})
}
},
created() {
this.getContact()
}
}
</script>
<style lang="scss" scoped>
.resident {
display: flex;
flex-direction: column;
height: 100%;
background: #F5F5F5;
.error {
font-size: 32px;
color: #666;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
::v-deep .u-scroll-box {
border-bottom: 1px solid #D4D4D4;
.u-tab-bar {
position: absolute;
bottom: -6px;
}
}
::v-deep .card {
background: #fff;
margin-bottom: 16px;
padding: 16px 32px;
}
.half {
width: 50%;
}
::v-deep .tag {
height: 56px;
line-height: 56px;
background: #F3F4F7;
border-radius: 6px;
padding: 8px 16px;
margin-right: 16px;
margin-bottom: 16px;
overflow: hidden;
cursor: default;
}
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<section class="residentList">
<ai-top-fixed>
<u-search placeholder="请输入昵称、姓名" :show-action="false" search-icon-color="#ccc"
v-model="search.name" @search="page.current=1,getList()"/>
<ai-cell>
<b slot="label" class="title"><i v-html="page.total||0"/>个居民</b>
</ai-cell>
</ai-top-fixed>
<div class="mainPane">
<ai-cell v-for="item in list" :key="item.id" @click.native="showResident(item)">
<template #label>
<ai-image :src="item.avatar" preview/>
</template>
<div class="card wrap start" flex>
<b>{{ item.name }}</b>
<div flex class="tag" v-for="(tag,j) in item.tags" :key="j">{{ tag.tagName }}</div>
<div class="realName" shrink v-html="`真实姓名:${item.realName||'-'}`"/>
</div>
</ai-cell>
</div>
</section>
</template>
<script>
import AiCell from "../../components/AiCell";
import AiImage from "../../components/AiImage";
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "residentList",
components: {AiTopFixed, AiImage, AiCell},
data() {
return {
page: {current: 1, size: 10, total: 0},
search: {name: ""},
list: []
}
},
methods: {
getList() {
this.$http.post("/app/wxcp/wxcustomer/list", null, {
params: {...this.page, ...this.search, type: 1}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
showResident({id}) {
id && uni.navigateTo({
url: './resident?id=' + id
})
}
},
created() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.residentList {
::v-deep .AiTopFixed {
b.title {
color: #333;
font-size: 32px;
& > i {
color: #267FCE;
font-style: normal;
margin: 0 4px;
}
}
}
::v-deep .mainPane {
background: #fff;
padding: 0 32px;
.AiCell {
flex-shrink: 0;
justify-content: flex-start;
.content {
flex: 1;
min-width: 0;
max-width: unset;
border-bottom: 1px solid rgba(221, 221, 221, 1);
min-height: 160px;
}
}
.card {
text-align: left;
b {
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 36px;
}
.tag {
justify-content: center;
background: #F3F4F7;
border-radius: 4px;
padding: 0 16px;
font-size: 28px;
font-weight: 400;
color: #333;
margin-left: 16px;
margin-bottom: 16px;
height: 56px;
}
.realName {
width: 100%;
margin-top: 8px;
font-size: 28px;
font-weight: 400;
color: #999;
}
}
.AiImage {
margin-right: 24px;
image {
width: 112px;
height: 112px;
}
}
}
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<div class="resident-statistical">
<u-tabs :list="tabs" :is-scroll="false" :current="currentType" font-size="32"
bar-width="192" height="96" bg-color="#3975C6"
active-color="#fff" inactive-color="#fff" @change="handleChange"/>
<div class="bg" :style="{backgroundImage:'url(' + $cdn + 'dvcp_bg.png' + ')'}"></div>
<div class="card">
<u-row justify="between">
<div class="item" v-for="(item,index) in cardList" :key="index">
<span :style="{color:item.color} ">{{ item.value }}</span>
<span>{{ item.label }}</span>
</div>
</u-row>
</div>
<div class="chart">
<div id="chart"></div>
</div>
</div>
</template>
<script>
import echarts from "echarts"
import {mapActions} from "vuex";
export default {
name: "residentSta",
props: {
isNormal: Boolean
},
data() {
return {
currentType: 0,
groupId: null,
chart: null,
chartData: null
}
},
methods: {
...mapActions(['injectJWeixin', "wxInvoke"]),
handleChange(i) {
this.currentType = i
this.getChart()
},
getChart() {
this.$http.post(this.currentType == 0 ? "/app/wxcp/wxgroup/groupStatistic" : "/app/wxcp/wxcustomerlog/customerStatistic", null, {
params: {
id: this.groupId
}
}).then(res => {
if (res.code == 0) {
this.chartData = res.data
this.initChart()
}
})
},
initChart() {
this.chart = echarts.init(document.getElementById('chart'))
this.setOptions()
},
setOptions() {
const x = Object.keys(this.chartData.list)
const y = Object.values(this.chartData.list)
this.chart.setOption({
backgroundColor: "#F9F9F9",
legend: {
data: this.currentType == 0 ? ["群成员总数", "入群人数", "退群人数"] : ["居民总数", "新增居民数", "流失居民数"],
icon: "rect",
itemWidth: 8,
itemHeight: 8,
itemGap: 10,
textStyle: {
fontSize: 14,
color: "#666666",
lineHeight: 20
},
bottom: 0
},
grid: {
left: 60,
top: "10%",
bottom: "30%",
},
xAxis: {
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
marginTop: 10,
rotate: 40,
textStyle: {
fontSize: 12,
color: '#666666'
}
},
data: x.map(e => e.slice(5))
},
yAxis: {
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
textStyle: {
fontSize: 12,
color: '#666666'
}
},
},
series: [
{
name: this.currentType == 0 ? "群成员总数" : "居民总数",
type: "line",
itemStyle: {
color: "#4B87FE"
},
data: y.map(e => e.total)
},
{
name: this.currentType == 0 ? "入群人数" : "新增居民数",
type: "line",
itemStyle: {
color: "#32C5FF"
},
data: y.map(e => e.increase)
},
{
name: this.currentType == 0 ? "退群人数" : "流失居民数",
type: "line",
itemStyle: {
color: "#FFAA44"
},
data: y.map(e => e.decrease)
}
]
})
}
},
computed: {
tabs() {
return [
{name: "居民群统计", value: 0},
{name: "居民统计", value: 1},
]
},
currentTab() {
return this.tabs.find(e => e.value == this.currentType)
},
cardList() {
return this.currentType == 0 ? [
{label: "群聊总数", value: this.chartData?.groupSum || "0", color: "#354FC7"},
{label: "群成员总数", value: this.chartData?.today?.total || "0", color: "#868686"},
{label: "今日入群", value: this.chartData?.today?.increase || "0", color: "#5FBA95"},
{label: "今日退群", value: this.chartData?.today?.decrease || "0", color: "#F09535"},
] : [
{label: "居民总数", value: this.chartData?.today?.total || "0", color: "#354FC7"},
{label: "今日新增", value: this.chartData?.today?.increase || "0", color: "#5FBA95"},
{label: "今日流失", value: this.chartData?.today?.decrease || "0", color: "#F09535"},
]
}
},
watch: {
isNormal(v) {
v && this.getChart()
}
},
created() {
if (this.$route.hash == "#dev") {
this.getChart()
} else this.injectJWeixin("getCurExternalChat").then(() => {
return Promise.resolve()
}).then(() => {
this.wxInvoke(['getCurExternalChat', {}, res => {
if (res?.err_msg == 'getCurExternalChat:ok') {
this.groupId = res.chatId
}
this.getChart()
}])
})
}
}
</script>
<style lang="scss" scoped>
.resident-statistical {
display: flex;
flex-direction: column;
background: #F5F5F5;
padding-top: 96px;
::v-deep .u-tabs {
position: fixed;
top: 0;
width: 100%;
z-index: 9;
}
.bg {
height: 340px;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.card {
width: 686px;
height: 232px;
margin: -140px auto 0;
background-color: #FFFFFF;
border-radius: 8px;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 32px;
.u-row {
width: 100%;
.item {
flex: 1;
text-align: center;
font-size: 40px;
font-weight: bold;
color: #354FC7;
& > span {
display: block;
}
& > span:last-child {
font-size: 30px;
font-weight: 500;
color: #999999;
line-height: 42px;
}
}
}
}
.chart {
background-color: #FFFFFF;
margin: 24px 0;
box-sizing: border-box;
padding: 32px;
display: flex;
justify-content: center;
align-items: center;
#chart {
width: 686px;
height: 470px;
}
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<section class="tagManage">
<div class="card" v-for="(group,i) in tags" :key="i">
<ai-cell title :label="group.name"/>
<u-row>
<div class="tag" v-for="(op,j) in group.tagList" :key="j" :class="{selected:selected.includes(op.id)}"
@tap="$u.debounce(handleToggle(op.id))">
{{ op.name }}
</div>
</u-row>
</div>
<ai-back custom @back="top.showTagManage=false"/>
</section>
</template>
<script>
import AiCell from "../../components/AiCell";
import UButton from "../../uview/components/u-button/u-button";
import ULoadingPage from "../../uview/components/u-loading-page/u-loading-page";
import AiBack from "../../components/AiBack";
export default {
name: "tagManage",
inject: ['top'],
components: {AiBack, ULoadingPage, UButton, AiCell},
data() {
return {
tags: []
}
},
computed: {
selected() {
return this.top.groupId ? this.top.detail?.tagList.map(e => e.tagId) : this.top.detail?.tags.map(e => e.tagId)
}
},
methods: {
getTags() {
this.$http.post(this.top.groupId ? "/app/wxcp/wxgroupchattag/listAll" : "/app/wxcp/wxcorptag/listAll", null, {
params: {size: 9999}
}).then(res => {
if (res?.data) {
this.tags = res.data.records
}
})
},
handleToggle(tagId) {
uni.showLoading({
title: '提交中'
});
this.$http.post(this.top.groupId ? "/app/wxcp/wxgroupchattag/markTagForCP" : "/app/wxcp/wxcorptag/markTagForCP", null, {
params: this.top.groupId ? {tagId, groupId: this.top.groupId,} : {tagId, customerId: this.top.custom}
}).then(res => {
uni.hideLoading()
if (res?.code == 0) {
this.$u.toast("操作成功!")
this.getTags()
this.top.getContact()
}
}).catch(err => {
uni.hideLoading()
this.$u.toast(err)
})
}
},
created() {
this.getTags()
}
}
</script>
<style lang="scss" scoped>
.tagManage {
padding-top: 16px;
.tag {
cursor: pointer !important;
&.selected {
background-color: $uni-color-primary !important;
color: #fff
}
&.disabled {
color: #999;
cursor: not-allowed;
}
& + .tag {
margin-left: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,192 @@
<template>
<div class="addPlay">
<div v-if="type == '1'">
<div class="content">
<div class="item">
<div class="label">音频文件</div>
<div class="value" @click="toRecord">
<span class="color-999" :style="{ color: file ? '#333' : '' }">{{ file ? '已选择' : '请选择' }}</span>
<img src="./img/right-img.png" alt="">
</div>
</div>
</div>
<div class="radio-content">
<div class="title">素材标题</div>
<textarea rows="2" placeholder="请输入30字以内" v-model="name" style="width:100%;height:80px;" maxlength="30"></textarea>
</div>
</div>
<div v-else>
<div class="radio-content mar-b16">
<div class="title">素材标题</div>
<textarea rows="2" placeholder="请输入30字以内" v-model="name" style="width:100%;height:80px;" maxlength="30"></textarea>
</div>
<div class="radio-content">
<div class="title">文本内容</div>
<textarea rows="8" placeholder="请输入文本内容12000字以内" v-model="content" style="width:100%;height:300px;" maxlength="12000"></textarea>
</div>
</div>
<div class="btn" @click="confirm">确认</div>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
export default {
name: "addPlay",
data() {
return {
type: '1',
file: null,
name: '',
content: ''
}
},
components: {
AiBack
},
onLoad (query) {
this.type = query.type
uni.$on('record', e => {
this.file = e
})
},
methods: {
toRecord () {
uni.navigateTo({
url: `/pages/resourcesManage/recording`
})
},
dataURLtoFile (dataurl, filename) {
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1]
var bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n)
while (n--){
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, {type:mime})
},
confirm () {
if (!this.file && this.type === '1') {
return this.$u.toast('请选择音频文件')
}
if (!this.name) {
return this.$u.toast('请输入素材标题')
}
if (!this.content && this.type === '3') {
return this.$u.toast('请输入文本内容')
}
uni.showLoading()
let formData = {}
formData = new FormData()
if (this.type === '1') {
formData.append('file', this.dataURLtoFile(this.file, this.name + '.mp3'))
formData.append('type', this.type)
formData.append('content', this.content)
formData.append('name', this.name)
} else {
formData.append('type', this.type)
formData.append('content', this.content)
formData.append('name', this.name)
}
this.$http.post(`/app/appdlbresource/addResourceWithFile`, formData).then((res) => {
uni.hideLoading()
if (res.code === 0) {
this.$u.toast('添加成功')
uni.$emit('getList')
uni.navigateBack({
delta: 1
})
}
}).catch(res => {
this.$u.toast(res)
uni.hideLoading()
})
}
}
}
</script>
<style lang="scss" scoped>
.addPlay {
padding-bottom: 128px;
.content{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
padding: 34px 0;
font-size: 32px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
line-height: 44px;
border-bottom: 1px solid #ddd;
display: flex;
color: #333;
justify-content: space-between;
.label{
width: 198px;
font-size: 32px;
}
.value{
font-size: 28px;
width: calc(100% - 198px);
padding-right: 32px;
box-sizing: border-box;
text-align: right;
img{
width: 32px;
height: 32px;
vertical-align: middle;
margin-left: 6px;
}
}
.color-999{
color: #999;
}
}
}
.radio-content{
padding: 34px 32px 38px;
background-color: #fff;
.title{
font-size: 32px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #333;
line-height: 44px;
margin-bottom: 32px;
span{
font-size: 24px;
font-weight: 400;
}
}
}
.mar-b16{
margin-bottom: 16px;
}
.btn{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 112px;
line-height: 112px;
text-align: center;
background: #3975C6;
font-size: 32px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #FFFFFF;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

View File

@@ -0,0 +1,540 @@
<template>
<div class="recording">
<div class="recording-content" v-if="isRecording">
<div class="header-top" :style="{width: progress + '%'}"></div>
<div class="header-line" :style="{width: progress + '%'}">
<span class="line1 animation"></span>
<span class="line2 animation"></span>
<span class="line3 animation"></span>
<span class="line4 animation"></span>
<span class="line5 animation"></span>
<span class="line6 animation"></span>
<span class="line7 animation"></span>
<span class="line8 animation"></span>
<span class="line9 animation"></span>
<span class="line10 animation"></span>
<span class="line11 animation"></span>
<span class="line12 animation"></span>
<span class="line13 animation"></span>
<span class="line14 animation"></span>
<span class="line15 animation"></span>
<span class="line16 animation"></span>
<span class="line17 animation"></span>
<span class="line18 animation"></span>
<span class="line19 animation"></span>
<span class="line20 animation"></span>
<span class="line1 animation"></span>
<span class="line2 animation"></span>
<span class="line3 animation"></span>
<span class="line4 animation"></span>
<span class="line5 animation"></span>
<span class="line6 animation"></span>
<span class="line7 animation"></span>
<span class="line8 animation"></span>
<span class="line9 animation"></span>
<span class="line10 animation"></span>
<span class="line11 animation"></span>
<span class="line12 animation"></span>
<span class="line13 animation"></span>
<span class="line14 animation"></span>
<span class="line15 animation"></span>
<span class="line16 animation"></span>
<span class="line17 animation"></span>
<span class="line18 animation"></span>
<span class="line19 animation"></span>
<span class="line20 animation"></span>
<span class="line1 animation"></span>
<span class="line2 animation"></span>
<span class="line3 animation"></span>
<span class="line4 animation"></span>
<span class="line5 animation"></span>
<span class="line6 animation"></span>
<span class="line7 animation"></span>
<span class="line8 animation"></span>
<span class="line9 animation"></span>
<span class="line10 animation"></span>
<span class="line10 animation"></span>
<span class="line11 animation"></span>
<span class="line12 animation"></span>
<span class="line13 animation"></span>
<span class="line14 animation"></span>
<span class="line15 animation"></span>
<span class="line16 animation"></span>
<span class="line17 animation"></span>
<span class="line18 animation"></span>
<span class="line19 animation"></span>
<span class="line20 animation"></span>
</div>
<div class="progress" :style="{left: progress + '%'}"></div>
<p class="time">{{ time }}</p>
</div>
<div class="stop-content" v-else-if="!isRecording && isStop">
<p class="time">{{ time }}</p>
<div class="header-line">
<span class="line1 animation"></span>
<span class="line2 animation"></span>
<span class="line3 animation"></span>
<span class="line4 animation"></span>
<span class="line5 animation"></span>
<span class="line6 animation"></span>
<span class="line7 animation"></span>
<span class="line8 animation"></span>
<span class="line9 animation"></span>
<span class="line10 animation"></span>
<span class="line11 animation"></span>
<span class="line12 animation"></span>
<span class="line13 animation"></span>
<span class="line14 animation"></span>
<span class="line15 animation"></span>
<span class="line16 animation"></span>
<span class="line17 animation"></span>
<span class="line18 animation"></span>
<span class="line19 animation"></span>
<span class="line20 animation"></span>
<span class="line1 animation"></span>
<span class="line2 animation"></span>
<span class="line3 animation"></span>
<span class="line4 animation"></span>
<span class="line5 animation"></span>
<span class="line6 animation"></span>
<span class="line7 animation"></span>
<span class="line8 animation"></span>
<span class="line9 animation"></span>
</div>
</div>
<div class="content" v-else>
<img src="./img/body.png" alt="">
<p class="text">点击下方按钮开始录音</p>
<p class="text">为了保证效果请靠近手机讲话哦~</p>
</div>
<div class="footer">
<span v-if="isRecording || isStop" @click="restart">重新开始</span>
<img v-if="isStop" :src="!isPlay ? playImg : stopImg" @click="play">
<img v-else :src="isRecording ? stopImg : microphone" @click="record">
<span v-if="isStop" @click="save">保存</span>
</div>
<AiBack></AiBack>
<audio style="opacity: 0; visibility: hidden;" :src="audioSrc" @ended="onAudioEnd"></audio>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
import Recorder from 'recorder-core'
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
import stopImg from './img/stop-img.png'
import microphone from './img/microphone.png'
import playImg from './img/play-icon.png'
export default {
name: 'recording',
data () {
return {
isRecording: false,
progress: 0,
microphone,
playImg,
stopImg,
isPlay: false,
isStop: false,
blobFile: null,
time: '00:00:00',
counterDownTime: 0,
recorder: null,
counterDownTimeout: null,
timingTimeout: null,
audioSrc: '',
audio: null,
duration: 0
}
},
components: {
AiBack
},
mounted () {
this.$nextTick(() => {
this.audio = document.querySelector('audio')
})
},
methods: {
record () {
if (!this.isRecording) {
this.duration = 0
this.recorder = Recorder({
type: 'mp3',
sampleRate: 16000,
bitRate: 16,
onProcess (buffers, powerLevel, bufferDuration, bufferSampleRate, newBufferIdx, asyncEnd) {
//可利用extensions/waveview.js扩展实时绘制波形
}
})
this.recorder.open(() => {
this.recorder.start()
this.isRecording = true
this.timing()
}, (msg, isUserNotAllow) => {
// this.$u.toast(`${isUserNotAllow ? '请同意授权' : msg}`)
})
} else {
this.stop()
}
},
blobToDataURI (blob) {
return new Promise((resolve) => {
var reader = new FileReader()
reader.readAsDataURL(blob)
reader.onload = function (e) {
resolve(e.target.result)
}
})
},
save () {
if (!this.blobFile) {
return this.$u.toast(`请录音`)
}
this.blobToDataURI(this.blobFile).then(res => {
uni.$emit('record', res)
uni.navigateBack({
delta: 1
})
})
},
onAudioEnd () {
this.isPlay = false
},
stop () {
this.isRecording = false
this.isStop = true
clearTimeout(this.timingTimeout)
this.recorder.stop((blob, duration) => {
this.blobFile = blob
var durationObj = this.$dayjs.duration(this.counterDownTime * 1000)
this.duration = this.counterDownTime
var hours = durationObj.hours() > 9 ? durationObj.hours() : '0' + durationObj.hours()
var min = durationObj.minutes() > 9 ? durationObj.minutes() : '0' + durationObj.minutes()
var seconds = durationObj.seconds() > 9 ? durationObj.seconds() : '0' + durationObj.seconds()
this.time = hours + ':' + min + ':' + seconds
console.log(blob, (window.URL|| webkitURL).createObjectURL(blob), '时长:' + duration + 'ms')
this.recorder.close()
this.recorder = null
}, msg => {
console.log('录音失败:' + msg)
this.recorder.close()
this.recorder = null
})
},
play () {
if (!this.isPlay) {
this.playAudio()
this.counterDown()
} else {
this.isPlay = false
clearTimeout(this.counterDownTimeout)
this.audio.pause()
}
},
playAudio () {
this.isPlay = true
if (!this.audioSrc) {
this.audioSrc = (window.URL||webkitURL).createObjectURL(this.blobFile)
}
this.$nextTick(() => {
this.audio.play()
})
},
recodeStop () {
this.recorder.stop((blob, duration) => {
this.restart()
this.blobFile = blob
console.log(blob, (window.URL|| webkitURL).createObjectURL(blob), '时长:' + duration + 'ms')
this.recorder.close()
this.recorder = null
}, msg => {
console.log('录音失败:' + msg)
this.recorder.close()
this.recorder = null
})
},
restart () {
this.time = '00:00:00'
this.progress = 0
this.blobFile = null
this.isPlay = false
this.isRecording = false
clearTimeout(this.counterDownTimeout)
clearTimeout(this.timingTimeout)
this.counterDownTime = 0
this.audioSrc = ''
this.isStop = false
},
counterDown () {
if (this.counterDownTime === -1) {
clearTimeout(this.counterDownTimeout)
this.counterDownTime = this.duration
return false
}
var durationObj = this.$dayjs.duration(this.counterDownTime * 1000)
var hours = durationObj.hours() > 9 ? durationObj.hours() : '0' + durationObj.hours()
var min = durationObj.minutes() > 9 ? durationObj.minutes() : '0' + durationObj.minutes()
var seconds = durationObj.seconds() > 9 ? durationObj.seconds() : '0' + durationObj.seconds()
this.time = hours + ':' + min + ':' + seconds
this.counterDownTime --
this.counterDownTimeout = setTimeout(() => {
this.counterDown()
}, 1000)
},
timing () {
this.progress = ((this.counterDownTime / 120) * 100).toFixed(2) > 100 ? 101 : ((this.counterDownTime / 120) * 100).toFixed(2)
var durationObj = this.$dayjs.duration(this.counterDownTime * 1000)
var hours = durationObj.hours() > 9 ? durationObj.hours() : '0' + durationObj.hours()
var min = durationObj.minutes() > 9 ? durationObj.minutes() : '0' + durationObj.minutes()
var seconds = durationObj.seconds() > 9 ? durationObj.seconds() : '0' + durationObj.seconds()
this.time = hours + ':' + min + ':' + seconds
this.counterDownTime ++
this.timingTimeout = setTimeout(() => {
this.timing()
}, 1000)
}
}
}
</script>
<style lang="scss" scoped>
uni-page-body{
background-color: #fff;
}
.recording {
height: 100vh;
overflow-y: hidden;
.content{
padding-top: 310px;
text-align: center;
img{
width: 406px;
height: 306px;
margin-bottom: 48px;
}
.text{
font-size: 30px;
color: #999;
line-height: 42px;
text-align: center;
}
}
.footer{
display: flex;
align-items: center;
justify-content: center;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 168px;
background: #F6F8FC;
padding: 20px 86px;
box-sizing: border-box;
span{
display: inline-block;
width: 160px;
height: 64px;
background: #4E8EEE;
border-radius: 32px;
font-size: 30px;
color: #fff;
text-align: center;
line-height: 64px;
vertical-align: top;
margin-top: 30px;
}
img{
width: 128px;
height: 128px;
margin: 0 64px;
}
}
.recording-content {
position: relative;
overflow: hidden;
}
.stop-content {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: calc(100% - 168px);
.header-line {
width: 380px;
}
.time {
margin-top: 0;
margin-bottom: 32px;
}
}
.header-top {
position: absolute;
top: 16px;
left: 0;
height: 6px;
background: url(./img/top.png) repeat-x;
background-size: auto 6px;
}
.time{
width: 100%;
margin-top: 208px;
text-align: center;
font-size: 96px;
color: #000000;
line-height: 134px;
}
.progress {
position: absolute;
top: 16px;
left: 0;
width: 2px;
height: 128px;
background: #3477EE;
}
.header-line {
width: 100%;
max-width: 100%;
height: 128px;
line-height: 128px;
margin-top: 16px;
overflow: hidden;
}
.animation {
animation: note 0.24s ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
.header-line span {
display: inline-block;
width: 6px;
height: 16px;
margin: 0 6px;
border: none;
border-radius: 4px;
background-color: #3476EE;
}
.header-line span.line1 {
animation-delay: -1s;
}
.header-line span.line2 {
animation-delay: -0.9s;
}
.header-line span.line3 {
animation-delay: -0.8s;
}
.header-line span.line4 {
animation-delay: -0.7s;
}
.header-line span.line5 {
animation-delay: -0.6s;
}
.header-line span.line6 {
animation-delay: -0.5s;
}
.header-line span.line7 {
animation-delay: -0.4s;
}
.header-line span.line8 {
animation-delay: -0.4s;
}
.header-line span.line9 {
animation-delay: -0.2s;
}
.header-line span.line10 {
animation-delay: -0.1s;
}
.header-line span.line11 {
animation-delay: -1s;
}
.header-line span.line12 {
animation-delay: -0.9s;
}
.header-line span.line13 {
animation-delay: -0.8s;
}
.header-line span.line14 {
animation-delay: -0.7s;
}
.header-line span.line15 {
animation-delay: -0.6s;
}
.header-line span.line16 {
animation-delay: -0.5s;
}
.header-line span.line17 {
animation-delay: -0.4s;
}
.header-line span.line18 {
animation-delay: -0.3s;
}
.header-line span.line19 {
animation-delay: -0.2s;
}
.header-line span.line20 {
animation-delay: -0.1s;
}
@keyframes note {
from {
transform: scaleY(1);
}
to {
transform: scaleY(4);
}
}
}
</style>

View File

@@ -0,0 +1,245 @@
<template>
<div class="resourcesManage">
<div class="tab">
<u-tabs :list="tab" :is-scroll="false" :current="currIndex" @change="change" height="96" :bar-style="barStyle"></u-tabs>
</div>
<div class="record" v-if="currIndex == 0">
<div class="item" v-for="(item, index) in list" :key="index" @click="choose(item)">
<img src="./img/play-icon.png" alt="">
<div class="info">
<p>{{ item.name ? item.name.split('.')[0] : '-' }}</p>
</div>
</div>
</div>
<div class="record-text" v-else>
<div class="item" v-for="(item, index) in list" :key="index" @click="choose(item)">
<div>{{ item.name }}</div>
<p>{{ item.content }}</p>
</div>
</div>
<img src="./img/add-icon.png" alt="" class="add-img" @click="add">
<u-popup v-model="isShow" mode="bottom">
<div class="audio">
<AiVideo :src="url" autoplay></AiVideo>
<!-- <audio :src="url" ref="audio" :controls="true" :name="autioName" style="display: block;"></audio> -->
</div>
</u-popup>
<AiBack></AiBack>
</div>
</template>
<script>
import AiVideo from '@/components/AiVideo'
import AiBack from "@/components/AiBack";
export default {
name: "resourcesManage",
data() {
return {
tab: [{ name: '音频素材' }, { name: '文本素材' }],
list: [],
currIndex: 0,
current: 1,
isChoose: false,
isMore: false,
isShow: false,
url: '',
autioName: '',
audio: null,
barStyle: {width:'98px', bottom: '-3px', left: '-38px'}
}
},
components: {
AiBack,
AiVideo
},
onLoad (query) {
this.isChoose = query.isChoose ? true : false
this.getList()
uni.$on('getList', e => {
this.isMore = false
this.list = []
this.current = 1
this.$nextTick(() => {
this.getList()
})
})
},
methods: {
change(index) {
this.isMore = false
this.list = []
this.current = 1
this.currIndex = index
this.$nextTick(() => {
this.getList()
})
},
add () {
uni.navigateTo({
url: `/pages/resourcesManage/addPlay?type=${this.currIndex === 0 ? 1 : 3}`
})
},
choose (item) {
if (!this.isChoose) {
console.log(item.url)
this.url = item.url
this.isShow = true
return false
}
uni.$emit('choose', {
mediaId: item.id,
mediaName: item.name
})
uni.navigateBack({
delta: 1
})
},
getList () {
if (this.isMore) return
this.$http.post(`/app/appdlbresource/list`, null, {
params: {
...this.search,
type: this.currIndex === 0 ? 1 : 3,
current: this.current,
size: 10
}
}).then(res => {
if (res.code == 0) {
if (this.current > 1) {
this.list = [...this.list, ...res.data.records]
} else {
this.list = res.data.records
}
uni.hideLoading()
if (res.data.records.length < 10) {
this.isMore = true
return false
}
this.current = this.current + 1
} else {
uni.hideLoading()
}
}).catch(() => {
uni.hideLoading()
})
}
},
onReachBottom() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.resourcesManage {
padding-bottom: 128px;
.tab{
border-bottom: 1px solid #ddd;
margin-bottom: 4px;
}
.audio {
width: 100%;
height: 400rpx;
box-sizing: border-box;
padding: 104rpx 0 46rpx;
audio {
}
}
.record{
background-color: #fff;
.item{
width: 100%;
display: flex;
border-bottom: 1px solid #ddd;
padding: 32px 30px;
box-sizing: border-box;
img{
width: 56px;
height: 56px;
margin-right: 14px;
}
.info{
width: calc(100% - 70px);
line-height: 44px;
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
}
}
}
}
.record-text{
background-color: #fff;
.item{
width: 100%;
border-bottom: 1px solid #ddd;
padding: 32px 30px;
box-sizing: border-box;
font-size: 30px;
div{
font-size: 30px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
margin-bottom: 12px;
.color-0063E5{
color: #0063E5;
}
.color-FF8100{
color: #FF8100;
}
.color-FF4466{
color: #FF4466;
}
}
p{
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #666;
line-height: 40px;
text-overflow: -o-ellipsis-lastline;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
word-break: break-all;
-webkit-box-orient: vertical;
}
}
}
.add-img{
width: 120px;
position: fixed;
bottom: 120px;
right: 32px;
}
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="talking">
<div class="header">
<p>刘家河村委会张家村<span>20</span>个设备</p>
<img src="./img/right-img.png" alt="">
</div>
<div class="content">
<!-- <p class="time">00:00:13</p> -->
<img src="./img/body.png" alt="">
<p class="text">请先选择设备</p>
<p class="text">再点击下方按钮开始喊话~</p>
</div>
<div class="footer">
<img src="./img/stop-img.png" alt="">
</div>
</div>
</template>
<script>
export default {
name: "talking",
data() {
return {
}
},
methods: {
},
mounted() {
}
}
</script>
<style lang="scss" scoped>
uni-page-body{
background-color: #fff;
}
.talking {
.header{
width: 100%;
height: 128px;
background: #F6F8FC;
padding: 42px 32px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
p{
font-size: 32px;
color: #333;
line-height: 44px;
span{
color: #1174FE;
}
}
img{
width: 32px;
height: 32px;
}
}
.content{
margin-top: 224px;
.time{
width: 100%;
text-align: center;
font-size: 96px;
color: #000000;
line-height: 134px;
}
img{
width: 406px;
height: 306px;
margin: 0 0 48px 172px;
}
.text{
font-size: 30px;
color: #999;
line-height: 42px;
text-align: center;
}
}
.footer{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 168px;
background: #F6F8FC;
padding: 20px 310px;
box-sizing: border-box;
img{
width: 128px;
height: 128px;
}
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="result-page">
<img :src="imgSrc" alt="">
<text>{{text}}</text>
<u-button type="primary" :custom-style="{width:'100%',borderRadius:'4px',marginTop:'48px'}" @click="goBack">{{btnText}}</u-button>
<back></back>
</div>
</template>
<script>
import Back from "../../../../components/AiBack"
export default {
name: "handlePage",
components: {Back},
data() {
return {
flag: true
}
},
onLoad(val) {
if (val.flag) {
this.flag = val.flag
}
},
methods: {
goBack() {
uni.navigateBack({
delta:2
})
}
},
computed: {
text(){
return this.flag ? '处理成功!' : '处理失败'
},
btnText(){
return this.flag ? '查看详情' : '我知道了'
},
imgSrc(){
return this.flag ? (this.imgOtherUrl + 'kztcg.png') : (this.imgOtherUrl + 'kztsb.png')
}
}
}
</script>
<style lang="scss" scoped>
.result-page {
min-height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
padding: 96px ;
img {
width: 192px;
height: 192px;
}
text{
font-size: 36px;
font-weight: 800;
color: #333333;
line-height: 50px;
display: flex;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,310 @@
<template>
<div class="report-wrapper">
<div class="report-form">
<div class="form-item">
<div class="form-item__left" shrink>
<i>*</i>
<label>上报类型</label>
</div>
<div class="form-item__right">
<ai-select :list="reportTypes" @data="selectReportType"/>
</div>
</div>
<div v-if="reportType" class="form-item">
<div class="form-item__left" shrink>
<i>*</i>
<label>事件类型</label>
</div>
<div class="form-item__right">
<ai-select :list="eventTypes" :value="eventType" @data="selectEventType"/>
</div>
</div>
<div class="form-item" v-if="!!eventType">
<div class="form-item__left">
<i>*</i>
<label>信用积分</label>
</div>
<div class="form-item__right">
<span class="score"> {{ integralType }}{{ integralScore }}</span>
</div>
</div>
<div class="form-area">
<div class="form-area__top">
<i>*</i>
<label>处理结果</label>
</div>
<textarea
placeholder="请输入处理结果描述200字以内"
:maxlength="200"
v-model="explain"
></textarea>
</div>
<div class="form-area">
<div class="form-area__top">
<!-- <i>*</i>-->
<label>图片上传最多9张</label>
</div>
<div class="uploader">
<ai-uploader
:limit="9"
multiple
@data="e=>files.push(e.file)"
@change="e=>files = e"
></ai-uploader>
</div>
</div>
</div>
<div bottom>
<u-button type="primary" @click="handleSubmit">提交</u-button>
</div>
<back></back>
</div>
</template>
<script>
import AiUploader from '../../components/AiUploader'
import Back from '../../components/AiBack'
import AiSelect from '../../components/AiSelect'
export default {
name: 'handleResult',
computed: {
eventTypes() {
return this.eventTypeList.map(e => ({value: e.id, label: e.ruleName}))
},
reportTypes() {
return this.$dict.getDict('integralDetailType').map(e => ({value: e.dictValue, label: e.dictName}))
},
integralScore() {
return (
this.eventTypeList?.find(e => e.id == this.eventType)?.integral || 0
)
},
integralType() {
return this.integralScore == 0
? ''
: this.eventTypeList?.find(e => e.id == this.eventType)?.integralType ==
0
? ''
: '+'
}
},
data() {
return {
eventType: '',//事件类型
reportType: '',//上报类型
dictList: [],
eventTypeList: [],
id: null,
reportIndex: null,
files: [],
explain: ''
}
},
components: {AiSelect, Back, AiUploader},
onLoad(opt) {
if (opt) {
let {id, reportType} = opt
this.id = id
this.reportType = reportType
this.$dict.load(['integralDetailType']).then(() => {
this.getDetailTypes(reportType)
})
}
},
methods: {
selectEventType(selecteds) {
this.eventType = selecteds?.[0]?.value
},
selectReportType(selecteds) {
this.reportType = ""
this.$nextTick(() => {
this.reportType = selecteds?.[0]?.value
this.eventType = ""
this.getDetailTypes(this.reportType)
})
},
getDetailTypes(classification) {
this.$http.post(`/app/appvillagerintegralrule/listForWx`, null, {
params: {classification, current: 1, size: 999, ruleStatus: 1}
}).then(res => {
if (res?.data) {
this.eventTypeList = res.data.records
}
})
},
handleSubmit() {
if (!this.reportType) {
return this.$u.toast("请选择上报类型")
}
if (!this.eventType) {
return this.$u.toast("请选择事件类型")
}
if (this.explain == '') {
return this.$u.toast("请填写处理结果")
}
this.$http.post(`/app/appreportatwillinfo/handle`, {
handleFiles: this.files,
reportType: this.reportType,
ruleId: this.eventType,
handleResult: this.explain,
id: this.id
})
.then(res => {
if (res?.code == 0) {
uni.navigateTo({
url: '/pages/snapshot/components/handlePage/handlePage'
})
}
})
.catch(e => {
this.$u.toast(e || '网络异常')
})
}
}
}
</script>
<style lang="scss" scoped>
.report-wrapper {
min-height: 100%;
padding-bottom: 112px;
box-sizing: border-box;
background-color: #f3f6f9;
.report-btn {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 112px;
line-height: 112px;
text-align: center;
color: #ffffff;
font-size: 34px;
font-weight: 600;
background: #135ab8;
}
}
.phone {
text-align: right;
}
.address {
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
::v-deep .picker {
display: flex;
align-items: center;
height: 112px;
line-height: 112px;
font-size: 32px;
span {
position: relative;
top: -10px;
font-size: 32px;
}
}
::v-deep .picker .AiArea {
display: flex;
align-items: center;
height: 112px;
line-height: 112px;
background-color: #fff !important;
}
.form-area {
margin-bottom: 16px;
padding: 34px 32px 34px 32px;
background: #fff;
.form-area__top {
display: flex;
align-items: center;
margin-bottom: 32px;
i {
margin-right: 4px;
color: #ff4466;
}
label {
color: #333333 !important;
font-size: 32px;
}
}
textarea {
width: 100%;
height: 140px;
padding-left: 16px;
}
.uploader {
padding-left: 16px;
}
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 112px;
margin-bottom: 16px;
padding: 0 32px 0 32px;
background: #fff;
.form-item__left {
display: flex;
align-items: center;
i {
margin-right: 4px;
color: #ff4466;
}
label {
color: #333333 !important;
font-size: 32px;
}
}
.form-item__right {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: 16px;
span {
color: #999999;
}
.score {
font-size: 34px;
font-weight: 600;
color: #e6736e;
}
}
::v-deep .AiSelect {
.display {
justify-content: flex-end;
}
.selectedLabel {
text-align: right;
}
}
}
</style>

View File

@@ -0,0 +1,281 @@
<template>
<div class="snapshot">
<div class="banner">
<picker :range="dictList" @change="bindPickerChange" range-key="dictName">
<div class="picker">
{{ reportType }}
<u-icon
name="arrow-down"
:custom-style="{ margin: '0 4px' }"
></u-icon>
</div>
</picker>
<u-search
placeholder="请输入标题"
:show-action="false"
v-model="keyword"
@clear="search"
@search="search"
/>
</div>
<u-tabs
class="nav"
:list="tabs"
:is-scroll="false"
:current="currentType"
font-size="32"
bar-width="192"
height="96"
@change="handleTabClick"
/>
<div class="body" v-if="eventList.length > 0">
<div
class="item"
v-for="(item, index) in eventList"
:key="index"
@click="detail(item)"
>
<u-row justify="between">
<u-row>
<img
class="avatar"
:src="item.portrait ? item.portrait : imgOtherUrl + 'tx.png'"
alt=""/>
<div class="wrap">
<span class="name">{{ item.nickName }}</span>
<span class="date">{{ item.createTime }}</span>
</div>
</u-row>
<u-row>
<u-icon :name="$cdn + 'Location2@2x.png'" :custom-style="{width:'20px',height:'20px'}"></u-icon>
<span class="date">{{item.areaName}}</span>
</u-row>
</u-row>
<div class="content">
<span class="text">
<span class="targ" v-if="item.reportType">{{
$dict.getLabel('atWillReportTypeForCp', item.reportType)
}}</span>
{{ item.explain }}</span>
</div>
<div class="photos">
<img
:src="photo.url"
v-for="(photo, idx) in item.files.slice(0,3)"
:key="idx"
@click.stop="previewImage(index, idx)"
/>
</div>
</div>
</div>
<AiEmpty v-else/>
</div>
</template>
<script>
import AiEmpty from '../../components/AiEmpty/AiEmpty'
import {mapState} from "vuex"
export default {
name: "snapshot",
components: {AiEmpty},
data() {
return {
dictList: [],
index: 0,
keyword: '',
currentCategory: '',
currentType: 0,
reportType: '上报类型',
reportIndex: '',
pageNum: 1,
pages: 2,
eventList: [],
}
},
onShow() {
this.$dict.load('atWillReportTypeForCp').then(() => {
this.dictList = this.$dict.getDict('atWillReportTypeForCp')
this.getList()
})
},
methods: {
detail(item) {
uni.navigateTo({
url: "/pages/snapshot/snapshotDetail?id=" + item.id
})
},
bindPickerChange(e) {
this.pageNum = 1;
this.pages = 2;
let index = e.detail.value
this.reportType = this.dictList[index].dictName
this.reportIndex = index
this.getList()
},
handleTabClick(i) {
this.currentType = i
this.pageNum = 1;
this.pages = 2;
this.getList()
},
search() {
this.pageNum = 1;
this.pages = 2;
this.getList()
},
previewImage(index, idx) {
uni.previewImage({
urls: this.eventList[index]['files'].map(e => e.url),
current: this.eventList[index]['files'][idx].url
})
},
getList() {
if (this.pageNum > this.pages) return
this.$http.post(`/app/appreportatwillinfo/list`, null, {
params: {
current: this.pageNum,
size: 10,
status: this.currentType,
reportType: this.reportIndex == 6 ? null : this.reportIndex,
address: this.keyword,
areaId:this.user?.areaId
}
}).then(res => {
if (res?.code == 0) {
const eventList = this.pageNum > 1 ? [...this.eventList, ...res.data.records] : res.data.records
this.pages = Math.ceil(res.data.total / 10)
this.eventList = eventList
}
})
}
},
onReachBottom() {
this.pageNum = this.pageNum + 1
this.getList()
},
computed: {
...mapState(["user"]),
tabs() {
return [
{name: "待处理", value: 0},
{name: "处理完成", value: 1},
]
},
},
}
</script>
<style lang="scss" scoped>
.snapshot {
min-height: 100%;
background: #F5F5F5;
.banner {
background-color: #ffffff;
height: 80px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 30px;
.picker {
font-size: 30px;
font-weight: 500;
color: #333333;
margin-right: 8px;
}
}
.nav {
background-color: #ffffff;
height: 96px;
padding-top: 8px;
}
.body {
.item {
background-color: #ffffff;
box-sizing: border-box;
padding: 30px;
margin-top: 16px;
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
margin-right: 8px;
}
.wrap {
display: flex;
flex-direction: column;
justify-content: center;
.name {
font-size: 26px;
font-weight: 500;
color: #333333;
line-height: 24px;
}
}
.date {
font-size: 22px;
color: #999999;
}
.content {
padding: 18px 0;
display: flex;
flex-wrap: wrap;
.text {
font-size: 26px;
font-weight: 400;
color: #333333;
line-height: 44px;
overflow: hidden;
display: -webkit-box;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
.targ {
padding: 6px;
background: #E8EFFF;
border-radius: 8px;
font-size: 24px;
font-weight: 400;
color: #2266FF;
margin-right: 8px;
line-height: 34px;
}
}
}
.photos {
display: flex;
flex-wrap: wrap;
& > img {
width: 224px;
height: 224px;
margin: 0 8px 8px 0;
&:nth-child(3n) {
margin-right: 0;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,226 @@
<template>
<div class="snapshot-detail">
<div class="info">
<div class="info-item">
<text>上报人</text>
<text>{{ detail.nickName }}</text>
</div>
<div class="info-item">
<text>联系电话</text>
<text>{{ detail.phone }}</text>
</div>
<div class="info-item">
<text>上报时间</text>
<text>{{ detail.createTime }}</text>
</div>
<div class="info-item">
<text>处理状态</text>
<text>{{ $dict.getLabel('reportAtWillHandleStatus', detail.status) }}</text>
</div>
</div>
<div class="body">
<u-row class="location" justify="space-between">
<text>{{ detail.address }}</text>
<u-icon :name="$cdn + 'Location2@2x.png'" :custom-style="{width:'24px',height:'24px'}"></u-icon>
</u-row>
<u-row justify="space-between" class="type" v-if="detail.reportType">
<text>上报类型</text>
<text>{{ $dict.getLabel('integralDetailType', detail.reportType) }}</text>
</u-row>
<text class="title">事件描述</text>
<div class="content">{{ detail.explain }}</div>
<div class="photos">
<img :src="item.url" alt="" v-for="(item,index) in detail.files" :key="index" @click.stop="previewImage(detail.files,index)">
</div>
<template v-if="detail.handleResult">
<u-row justify="space-between" class="result">
<text class="title">处理结果</text>
<text>{{detail.integral|formt}}</text>
</u-row>
<div class="content">{{ detail.handleResult }}</div>
<div class="photos">
<img :src="item.url" alt="" v-for="(item,index) in detail.handleFiles" :key="index"
@click.stop="previewImage(detail.handleFiles,index)">
</div>
<u-row justify="space-between" class="type">
<div shrink>事件类型</div>
<text>{{ detail.ruleName }}</text>
</u-row>
<u-row justify="space-between" class="type">
<text>处理人</text>
<text class="nonmal">{{ detail.handleUserName }}</text>
</u-row>
<!-- <u-row justify="space-between" class="type">-->
<!-- <text>联系电话</text>-->
<!-- <text class="nonmal">18707170017</text>-->
<!-- </u-row>-->
<u-row justify="space-between" class="type">
<text>处理时间</text>
<text class="nonmal">{{ detail.handleTime }}</text>
</u-row>
</template>
</div>
<div bottom v-if="!detail.handleResult">
<u-button type="primary" v-if="!detail.handleResult" @click="handle">去处理</u-button>
</div>
<back></back>
</div>
</template>
<script>
import Back from "../../components/AiBack";
export default {
name: "snapshotDetail",
components: {Back},
onLoad(opt) {
if (opt.id) {
this.id = opt.id
}
},
onShow() {
this.$dict.load('reportAtWillHandleStatus', 'integralDetailType').then(() => {
this.getDetail()
})
},
data() {
return {
detail: {},
id: null,
}
},
methods: {
previewImage(data,index) {
uni.previewImage({
urls: data.map(e => e.url),
current: data[index].url
})
},
handle() {
uni.navigateTo({
url: `/pages/snapshot/handleResult?id=${this.id}&reportType=${this.detail.reportType}`
})
},
getDetail() {
this.$http.post(`/app/appreportatwillinfo/queryDetailById`, null, {
params: {
id: this.id
}
}).then(res => {
if (res.code == 0) {
this.detail = res.data
}
})
}
},
filters:{
formt(val){
return +val > 0 ? `+${val}` : val
}
}
}
</script>
<style lang="scss" scoped>
.snapshot-detail {
min-height: 100%;
background-color: #F3F6F9;
padding-bottom: 160px;
.info {
height: 384px;
background-color: #3975C6;
box-sizing: border-box;
padding: 32px;
.info-item {
font-size: 28px;
font-weight: 400;
color: #FFFFFF;
line-height: 40px;
margin-bottom: 16px;
}
}
.body {
width: 686px;
margin: 0 auto;
margin-top: -96px;
background: #FFFFFF;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.02);
border-radius: 8px;
box-sizing: border-box;
padding: 34px 32px;
.location {
& > text {
font-size: 32px;
font-weight: 800;
color: #333333;
line-height: 44px;
}
}
.type {
font-size: 32px;
margin: 68px 0;
& > text:nth-child(1) {
color: #666666;
}
& > text:nth-child(2) {
color: #135AB8;
}
.nonmal {
color: #333333 !important;
}
}
.title {
font-size: 32px;
color: #666666;
}
.content {
margin-top: 10px;
font-size: 32px;
color: #333333;
line-height: 44px;
word-break: break-all;
}
.photos {
display: flex;
flex-wrap: wrap;
margin: 30px 0;
& > img {
width: 196px;
height: 196px;
margin: 0 14px 14px 0;
flex-shrink: 0;
&:nth-child(3n) {
margin-right: 0;
}
}
}
.result {
margin-top: 48px;
& > text:nth-child(2) {
font-size: 34px;
font-weight: 600;
color: #E6736E;
}
}
}
}
</style>

View File

@@ -0,0 +1,286 @@
<template>
<div class="balance">
<div class="operate">
<div class="target">结算对象</div>
<div class="select" @click="handleSelect">
{{
selected
? `${selected.familyName} 剩余积分:${selected.familyIntegral}`
: '请选择'
}}
<u-icon name="arrow-right" color="#E2E2E2" size="28"></u-icon>
</div>
</div>
<div class="line" :style="{ backgroundImage: 'url(' + uri + ')' }"></div>
<scroll-view scroll-y class="category-wrap">
<div
class="category-item"
v-for="(item, index) in categoryList"
:key="index"
>
<img class="category-img" :src="parseObj(item.photo)" alt="" />
<div class="category-info">
<label class="hidden">{{ item.merchandiseName }}</label>
<span class="score"
>{{ item.costIntegral }}
<span>积分</span>
</span>
<div class="wrap">×{{ item.merchandiseNumber }}</div>
</div>
</div>
</scroll-view>
<div class="footer">
<div class="sum">
<span>{{ totalCount }}件商品</span>
<span
>合计{{ totalScore }}
<span>积分</span>
</span>
</div>
<div class="btn" @click="hanldeSubmit">确认领取</div>
</div>
<back />
</div>
</template>
<script>
import Back from '../../components/AiBack'
export default {
name: 'balance',
components: { Back },
data() {
return {
selected: null,
categoryList: [],
totalScore: 0,
totalCount: 0
}
},
onLoad(opt) {
if (opt.category) {
let sum = 0
let count = 0
this.categoryList = JSON.parse(opt.category)
this.categoryList.map(e => {
count += e.merchandiseNumber
if (e.merchandiseNumber != 0) {
sum += e.merchandiseNumber * e.costIntegral
}
})
this.totalScore = sum
this.totalCount = count
}
uni.$on('selected', data => {
if (data) {
this.selected = data
}
})
},
computed: {
uri() {
return this.imgOtherUrl + 'line.png'
}
},
methods: {
parseObj(json) {
return JSON.parse(json || '[]')[0]?.url
},
hanldeSubmit() {
if (!this.selected)
return uni.showToast({
title: '请选择结算对象',
icon: 'none'
})
let { memberId, familyId } = this.selected
this.$http
.post(`/app/appvillagerintegralshoporder/addOrder`, {
shopId: this.categoryList[0].shopId,
memberId,
familyId,
orderIntegral: this.totalScore,
merchandiseList: this.categoryList.map(e => {
return {
...e,
merchandiseId: e.id
}
})
})
.then(res => {
if (res.code == 0) {
uni.navigateTo({
url: '/pages/supermarket/components/resultPage/resultPage'
})
} else {
uni.navigateTo({
url:
'/pages/supermarket/components/resultPage/resultPage?flag=' +
false
})
}
})
.catch(e => {
uni.showToast({
title: e || '网络异常',
icon: 'none'
})
})
},
handleSelect() {
uni.navigateTo({
url: '/pages/supermarket/search'
})
}
}
}
</script>
<style lang="scss" scoped>
.balance {
min-height: 100%;
background-color: #ffffff;
.operate {
height: 120px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 30px;
.target {
font-size: 32px;
color: #333333;
&:before {
content: '*';
font-size: 32px;
color: #ff4466;
}
}
.select {
font-size: 32px;
color: #666666;
& > i {
font-size: 20px;
}
}
}
.line {
// margin: 0 30px;
padding: 0 15px;
height: 8px;
background-repeat: no-repeat;
background-size: cover;
}
.category-wrap {
padding: 16px 0 110px;
.category-item {
box-sizing: border-box;
padding: 28px 32px 44px 30px;
height: 264px;
display: flex;
.category-img {
width: 192px;
height: 192px;
border: 1px solid #f6f6f6;
flex-shrink: 0;
margin-right: 30px;
}
.category-info {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
& > label {
font-size: 30px;
font-weight: 800;
color: #333333;
line-height: 42px;
}
.score {
font-size: 40px;
font-weight: 600;
color: #fa4a51;
& > span {
font-size: 24px;
color: #fa4a51;
margin-left: 8px;
}
}
.wrap {
display: flex;
justify-content: flex-end;
font-weight: 600;
}
}
}
}
.footer {
display: flex;
align-items: center;
width: 100%;
background-color: #ffffff;
z-index: 100;
height: 104px;
box-shadow: 0px -2px 8px 0px rgba(214, 214, 214, 0.5);
position: fixed;
left: 0;
bottom: 0;
.sum {
width: calc(100% - 212px);
box-sizing: border-box;
padding: 30px;
display: flex;
justify-content: space-between;
& > span:nth-child(1),
span:nth-child(2) {
font-size: 32px;
font-weight: 400;
color: #f94246;
& > span {
font-size: 24px;
margin-left: 12px;
}
}
}
.btn {
width: 212px;
height: 100%;
background-color: #1365dd;
font-size: 36px;
font-weight: 500;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
}
.hidden {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="result-page">
<img :src="imgSrc" alt="">
<text>{{text}}</text>
<u-button type="primary" :custom-style="{width:'100%',borderRadius:'4px',marginTop:'48px'}" @click="goBack">{{btnText}}</u-button>
<back></back>
</div>
</template>
<script>
import Back from "../../../../components/AiBack";
export default {
name: "result-page",
components: {Back},
data() {
return {
flag: true
}
},
onLoad(val) {
if (val.flag) {
this.flag = val.flag
}
},
methods: {
goBack() {
uni.navigateBack({
delta:3
})
}
},
computed: {
text(){
return this.flag ? '领取成功!' : '领取失败!请联系管理员处理'
},
btnText(){
return this.flag ? '确定' : '我知道了'
},
imgSrc(){
return this.flag ? (this.imgOtherUrl + 'kztcg.png') : (this.imgOtherUrl + 'kztsb.png')
}
}
}
</script>
<style lang="scss" scoped>
.result-page {
min-height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
padding: 96px ;
img {
width: 192px;
height: 192px;
}
text{
font-size: 36px;
font-weight: 800;
color: #333333;
line-height: 50px;
display: flex;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="search">
<div class="search-wrap">
<u-search :show-action="false" placeholder="请输入手机号或身份证号搜索" v-model.trim="keyword" @search="search"></u-search>
</div>
<div class="result-body">
<div class="res-item" v-if="result" @click="selected">
<span>{{result.familyName}}</span>
<span>剩余积分:{{result.familyIntegral}}</span>
</div>
<span class="placeholder" v-else>请通过搜索
<strong>手机号或身份证号</strong>
后选择结算对象
</span>
</div>
<u-modal v-model="show" title="温馨提示" @confirm="confirm" content="该手机号绑定了多个居民,请通过身份证号进行查询"
:confirm-style="{fontWeight:800}"></u-modal>
<back/>
</div>
</template>
<script>
import back from "../../components/AiBack";
export default {
name: "search",
components: {back},
data() {
return {
keyword: "",
show: false,
result: null,
}
},
methods: {
selected() {
uni.navigateBack({
delta: 1,
success: () => {
uni.$emit('selected',this.result)
},
fail: (err) => {
console.error(err)
}
})
},
search() {
if (this.keyword == "") {
return uni.showToast({
title: "请输入搜索关键字",
icon: 'none'
})
}
this.$http.post(`/app/appresident/queryFamilyByPhone`, null, {
params: {
phone: this.keyword
}
}).then(res => {
if (res && res.data) {
this.result = res.data
}
}).catch(e => {
this.result = null
uni.showToast({
title: e,
icon: 'none'
})
})
},
confirm() {
this.show = false
}
}
}
</script>
<style lang="scss" scoped>
.search {
min-height: 100%;
background-color: #ffffff;
.search-wrap {
width: 100%;
box-sizing: border-box;
padding: 24px 32px;
}
.result-body {
.res-item {
height: 112px;
border-bottom: 1px solid #F5F5F5;
box-sizing: border-box;
padding: 0 56px;
display: flex;
align-items: center;
justify-content: space-between;
& > span {
font-size: 32px;
color: #333333;
}
}
.placeholder {
display: flex;
justify-content: center;
margin-top: 144px;
font-size: 28px;
color: #999999;
& > strong {
color: #135AB8;
}
}
}
}
</style>

View File

@@ -0,0 +1,337 @@
<template>
<section class="supermarket">
<template v-if="Object.keys(list).length">
<div class="nav-left">
<scroll-view scroll-y style="height: 100%;">
<div class="nav-left-item" v-for="(val, key, index) in list" :style="active(key)" :key="index"
@click="clickSort(key,index)">
{{key}}分区
<u-badge size="mini" :count="sortCountList[index]" absolute :offset="[5,5]"></u-badge>
</div>
</scroll-view>
</div>
<div class="nav-right">
<scroll-view scroll-y style="height: 100%;">
<div class="category-item" v-for="(item,index) in categoryList" :key="index">
<img
:src="parseObj(item.photo)"
class="category-img" alt="">
<div class="category-info">
<label class="hidden">{{item.merchandiseName}}</label>
<span class="score">{{item.costIntegral}}
<span>积分</span>
</span>
<div class="wrap">
<div class="lxc-count">
<div class="less" @click="less(item,index)">-</div>
<div class="num">{{item.merchandiseNumber}}</div>
<div class="less" @click="add(item,index)">+</div>
</div>
</div>
</div>
</div>
</scroll-view>
</div>
<div class="footer">
<div class="sum">
<span>{{totalCount}}件商品</span>
<span>合计{{totalScore}}
<span>积分</span>
</span>
</div>
<div class="btn" @click="handleSubmit">去结算</div>
</div>
</template>
<AiEmpty v-else></AiEmpty>
</section>
</template>
<script>
import AiEmpty from "../../components/AiEmpty/AiEmpty";
import {mapState} from "vuex";
export default {
name: "supermarket",
components: {AiEmpty},
data() {
return {
list: {},
idx: 0,
totalScore: 0,
sortCountList: [],
mark: 0,
}
},
onLoad() {
this.getList()
},
watch: {
list: {
handler(val) {
let sum = 0
Object.keys(val).map(e=>{
val[e].map(p=>{
if (p.merchandiseNumber != 0) {
sum += (p.merchandiseNumber) * (p.costIntegral)
}
})
})
this.totalScore = sum
},
immediate: true,
deep: true
}
},
computed: {
...mapState(['user']),
totalCount() {
return this.sortCountList.reduce((pre, cur) => (pre + cur), 0)
},
categoryList() {
return this.idx == 0 ? this.list[Object.keys(this.list)[0]] : this.list[this.idx]
},
},
methods: {
handleSubmit() {
if (this.totalCount == 0) {
return uni.showToast({
title: "您还没有选择商品",
icon: 'none'
})
}
let filter = []
Object.keys(this.list).map(e => {
this.list[e].map(p => {
if (p.merchandiseNumber > 0) {
filter.push(p)
}
})
})
uni.navigateTo({
url: "/pages/supermarket/balance?category=" + JSON.stringify(filter)
})
},
active(key) {
const flag = key == this.idx
return {
borderLeft: flag ? '3px solid #1D58FE' : '',
background: flag ? 'linear-gradient(270deg, #FFFFFF 0%, #FFFFFF 77%, #E7EAFA 100%)' : ''
}
},
add(item, index) {
this.list[this.idx][index]["merchandiseNumber"] = this.list[this.idx][index]["merchandiseNumber"] + 1
this.$set(this.sortCountList, this.mark, this.list[this.idx]?.reduce((pre,curr)=>{
return (pre + curr.merchandiseNumber)
},0))
},
less(item, index) {
if (item.merchandiseNumber > 0) {
this.list[this.idx][index]["merchandiseNumber"] = this.list[this.idx][index]["merchandiseNumber"] - 1
this.$set(this.sortCountList, this.mark, this.list[this.idx]?.reduce((pre,curr)=>{
return (pre + curr.merchandiseNumber)
},0))
}
},
clickSort(key, index) {
this.mark = index
this.idx = key.toString()
},
parseObj(json) {
return JSON.parse(json || '[]')[0]?.url
},
getList() {
this.$http.post(`/app/appvillagerintegralmerchandise/listByIntegral`, null, {
params: {
areaId: this.user?.areaId
}
}).then(res => {
if (res && res.data) {
Object.keys(res.data).map(e => {
res.data[e].map(p => {
p.merchandiseNumber = 0
})
})
this.list = res.data
this.idx = Object.keys(res.data)[0]
this.sortCountList = Array(Object.keys(res.data).length).fill(0)
}
})
}
},
}
</script>
<style lang="scss" scoped>
uni-page-body{
background-color: #ffffff;
}
.supermarket {
background-color: #ffffff;
display: flex;
justify-content: space-between;
padding-bottom: 104px;
.nav-left {
width: 168px;
height: 100%;
.nav-left-item {
height: 104px;
background: #FAF9FB;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #333333;
border-top: 1px solid #D8E5FF;
border-left: 2px solid transparent;
position: relative;
&:nth-last-child {
border-bottom: 1px solid #D8E5FF;
}
}
}
.nav-right {
width: calc(100% - 168px - 24px);
height: 100%;
.category-item {
height: 264px;
box-sizing: border-box;
padding: 28px 32px 44px 30px;
display: flex;
.category-img {
width: 192px;
height: 192px;
border: 1px solid #F6F6F6;
flex-shrink: 0;
margin-right: 30px;
}
.category-info {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
& > label {
font-size: 30px;
font-weight: 800;
color: #333333;
line-height: 42px;
}
.score {
font-size: 40px;
font-weight: 600;
color: #FA4A51;
line-height: 40px;
& > span {
font-size: 24px;
color: #FA4A51;
margin-left: 8px;
}
}
.wrap {
display: flex;
justify-content: flex-end;
.lxc-count{
display: flex;
align-items: center;
justify-content: center;
.less{
width: 40px;
height: 40px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: 500;
color: #333333;
}
.num{
width: 89px;
height: 61px;
display: flex;
align-items: center;
justify-content: center;
background: #F6F6F6;
border-radius: 10px;
font-size: 28px;
font-weight: 400;
color: #333333;
margin: 0 16px;
}
}
}
}
}
}
.footer {
display: flex;
align-items: center;
width: 100%;
background-color: #ffffff;
z-index: 100;
height: 104px;
box-shadow: 0px -2px 8px 0px rgba(214, 214, 214, 0.5);
position: fixed;
left: 0;
bottom: 0;
.sum {
width: calc(100% - 212px);
box-sizing: border-box;
padding: 30px;
display: flex;
justify-content: space-between;
& > span:nth-child(1), span:nth-child(2) {
font-size: 32px;
font-weight: 400;
color: #F94246;
& > span {
font-size: 24px;
margin-left: 12px;
}
}
}
.btn {
width: 212px;
height: 100%;
background-color: #1365DD;
font-size: 36px;
font-weight: 500;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
}
.hidden {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
</style>

View File

@@ -0,0 +1,87 @@
<template>
<section class="monitorDetail">
<div class="videoBox">
<iframe ref="monitorIns" :style="style" :src="monitor.url" allow="autoplay *; microphone *; fullscreen *"
allowfullscreen allowtransparency allowusermedia frameBorder="no"/>
</div>
</section>
</template>
<script>
import AiFixedBtn from "../../components/AiFixedBtn";
export default {
name: "monitorDetail",
components: {AiFixedBtn},
data() {
return {
style: {},
monitor: {}
}
},
mounted() {
this.$nextTick(() => {
this.detectOrient()
})
},
methods: {
getDetail(deviceId) {
deviceId && this.$http.post("/app/appzyvideoequipment/getWebSdkUrl", null, {
params: {deviceId}
}).then(res => {
if (res?.data) {
this.monitor = JSON.parse(res.data)
}
})
},
detectOrient() {
var width = document.querySelector('.monitorDetail').clientWidth
var height = document.querySelector('.monitorDetail').clientHeight
if (width >= height) { // 竖屏
this.style = {
width: '100%',
height: '100%',
transform: 'rotate(0eg)',
transformOrigin: `0 0`
}
} else {
this.style = {
width: height + 'px',
height: width + 'px',
transform: 'rotate(90deg)',
transformOrigin: `${width / 2}px ${width / 2}px`
}
}
}
},
onLoad(params) {
this.getDetail(params.id)
}
}
</script>
<style lang="scss" scoped>
.monitorDetail {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100vw;
height: 100vh;
background: #000;
color: #fff;
.videoBox {
width: 100vw;
height: 100vh;
iframe {
}
}
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<section class="videoSurveillance">
<ai-top-fixed>
<div class="header" flex>
<div flex v-for="(node,i) in nodes" :key="i">
<div :class="{current:isCurrent(i)}" v-text="node.nodeName" @click="gotoNode(node,i)"/>
<u-icon v-if="!isCurrent(i)" name="arrow-right" color="#ddd"/>
</div>
</div>
</ai-top-fixed>
<div class="list">
<div class="item" v-for="row in monitors" :key="row.nodeId" :class="{online:!row.online}">
<template v-if="!!row.deviceId">
<img src="../resourcesManage/img/video-img.png" alt="" class="videoIcon" @click="showMonitor(row)">
<div class="area-name" v-text="row.deviceName" @click="showMonitor(row)"/>
<div class="deviceStatus" v-text="!row.online?'在线':'离线'" @click="showMonitor(row)"/>
</template>
<template v-else>
<div class="area-name" v-text="row.nodeName" @click="getMore(row)"/>
<u-icon name="arrow-right" color="#ddd" @click="getMore(row)"/>
</template>
</div>
</div>
</section>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "videoSurveillance",
components: {AiTopFixed},
data() {
return {
nodes: [
{nodeName: "首页"}
],
monitors: []
}
},
methods: {
getMonitors(nodeId, queryType = 0) {
this.monitors = []
this.$http.post("/app/appzyvideoequipment/getTree", {
nodeId, queryType
}).then(res => {
if (res?.data) {
this.monitors = JSON.parse(res.data)?.[queryType == 0 ? 'node' : 'device'] || []
}
})
},
getMore(row) {
this.nodes.push(row)
this.getMonitors(row.nodeId, row.hasChild == 1 ? 0 : 1)
},
showMonitor(row) {
uni.navigateTo({url: `./monitorDetail?id=${row.deviceId}`})
},
isCurrent(index) {
return index == Math.max(this.nodes?.length - 1, 0)
},
gotoNode(node, index) {
this.nodes.splice(index + 1)
this.getMonitors(node.nodeId)
}
},
created() {
this.getMonitors()
}
}
</script>
<style lang="scss" scoped>
.videoSurveillance {
::v-deep .placeholder {
margin-bottom: 8px;
}
.header {
color: #666;
.current {
color: #4E8EEE;
}
}
.list {
padding-left: 32px;
background: #FFF;
.item {
width: 100%;
height: 104px;
border-bottom: 1px solid #DDD;
padding: 0 32px 0 0;
box-sizing: border-box;
display: flex;
align-items: center;
&.online {
.videoIcon {
filter: none;
}
.deviceStatus {
color: #4E8EEE;
background: #E7F1FD;
}
}
.videoIcon {
width: 48px;
height: 32px;
margin-right: 8px;
filter: grayscale(100%);
}
.area-name {
font-size: 34px;
font-weight: 500;
color: #333;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.deviceStatus {
margin-left: 8px;
height: 22px;
padding: 0 8px;
line-height: 22px;
font-size: 13px;
color: #666;
background: #E9E9E9;
}
&:last-of-type {
border-bottom: none;
}
}
}
}
</style>

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