Merge branch 'dev' into vite

# Conflicts:
#	examples/router/autoRoutes.js
#	package.json
#	packages/bigscreen/designer/components/Add.vue
#	project/dv/apps/AppGridDV.vue
#	vue.config.js
This commit is contained in:
aixianling
2022-08-23 11:14:38 +08:00
125 changed files with 15445 additions and 4898 deletions

View File

@@ -1,175 +1,29 @@
<template>
<section class="AppMenuManager">
<ai-list>
<ai-title slot="title" title="菜单配置" isShowBottomBorder/>
<template #content>
<ai-search-bar>
<template #left>
<el-button type="primary" icon="el-icon-circle-plus" @click="addRootMenu">添加一级目录</el-button>
</template>
<template #right>
<el-input size="small" v-model="search" clearable @change="$refs.MenuTree.filter(search)"
placeholder="菜单名称"/>
<el-button icon="iconfont iconResetting" @click="getData">刷新</el-button>
</template>
</ai-search-bar>
<el-row type="flex" class="headerRow">
<div class="menuName" v-text="`菜单名称`"/>
<el-row type="flex" align="middle" class="info">
<div class="style" v-text="`图标`"/>
<div class="type" v-text="`菜单类型`"/>
<div class="component" v-text="`应用模块`"/>
<div class="status" v-text="`是否显示`"/>
<div class="showIndex" v-text="`排序`"/>
</el-row>
<div class="operation" v-text="`操作`"/>
</el-row>
<el-scrollbar>
<el-tree ref="MenuTree" :data="treeData" :props="{children:'subSet'}" highlight-current node-key="id"
:filter-node-method="handleSearch">
<el-row type="flex" align="middle" slot-scope="{node,data}" class="menuItem">
<div class="menuName" v-text="data.name"/>
<el-row type="flex" align="middle" class="info">
<div class="style" :class="data.style"/>
<div class="type" v-text="dict.getLabel('menuType',data.type)"/>
<div class="component" v-text="data.component"/>
<div class="status" v-text="dict.getLabel('yesOrNo',data.status)"/>
<div class="showIndex" v-text="data.showIndex"/>
</el-row>
<el-row type="flex" align="middle" class="operation">
<div v-if="node.isLeaf" class="opBtn del" v-text="`删除`" @click="handleDelete(data)"/>
<div v-if="data.type<2" class="opBtn" v-text="`添加下级`" @click="addMenu(data)"/>
<div class="opBtn" v-text="`编辑`" @click="handleEdit(data)"/>
</el-row>
</el-row>
</el-tree>
</el-scrollbar>
</template>
</ai-list>
<ai-dialog :visible.sync="dialog" title="菜单设置" width="500px" @onConfirm="handleSubmit"
@closed="form={},selected={}">
<el-form ref="MenuForm" :model="form" size="small" label-width="100px" :rules="rules">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" clearable/>
</el-form-item>
<el-form-item label="菜单类型" prop="type">
<ai-select v-model="form.type" clearable :selectList="dict.getDict('menuType')"/>
</el-form-item>
<template v-if="form.type==0">
<el-form-item label="菜单图标" prop="style">
<el-input v-model="form.style" placeholder="请输入" clearable/>
</el-form-item>
</template>
<template v-if="form.type==1">
<el-form-item label="菜单应用" prop="component">
<el-input v-model="form.component" placeholder="请输入" clearable/>
</el-form-item>
<el-form-item label="路径(path)" prop="path">
<el-input v-model="form.path" placeholder="请输入" clearable/>
</el-form-item>
</template>
<template v-if="form.type==2">
<el-form-item label="权限码" prop="permission">
<el-input v-model="form.permission" placeholder="请输入" clearable/>
</el-form-item>
</template>
<el-form-item label="显示菜单" prop="status">
<ai-select v-model="form.status" clearable :selectList="dict.getDict('yesOrNo')"/>
</el-form-item>
<el-form-item v-if="form.type<2" label="排序" prop="showIndex">
<el-input v-model="form.showIndex" placeholder="请输入" clearable/>
</el-form-item>
</el-form>
</ai-dialog>
<component :is="currentPage" v-bind="$props"/>
</section>
</template>
<script>
import List from "./list";
import IntroPage from "./introPage";
export default {
name: "AppMenuManager",
components: {IntroPage, List},
label: "菜单管理",
props: {
instance: Function,
dict: {default: () => ({})}
},
data() {
return {
treeData: [],
dialog: false,
form: {},
selected: {},
rules: {
name: [{required: true, message: "请输入 菜单名称"}],
type: [{required: true, message: "请选择 菜单类型"}],
status: [{required: true, message: "请选择 显示菜单"}],
showIndex: [{required: true, message: "请输入 排序"}],
permission: [{required: true, message: "请输入 权限码"}],
},
search: ""
}
},
methods: {
getData() {
return this.instance.post("/admin/menu/menuTree").then(res => {
if (res?.data) {
this.treeData = res.data
}
})
},
handleSubmit() {
this.$refs.MenuForm.validate(v => {
if (v) {
this.instance.post("/admin/menu/addOrUpdate", this.form).then(res => {
if (res?.code == 0) {
this.$message.success("提交成功!")
this.dialog = false
if (!!this.form.id) {
let node = this.$refs.MenuTree.getNode(this.form)
node.data = this.form
} else if (!!this.form.parentId) {
this.$refs.MenuTree.append(this.form, this.selected)
} else this.getData()
}
})
}
})
},
handleDelete(data) {
let {id} = data
this.$confirm("是否要删除该菜单").then(() => {
this.instance.post("/admin/menu/delete", null, {
params: {id}
}).then(res => {
if (res?.code == 0) {
this.$message.success("删除成功!")
this.dialog = false
this.$refs.MenuTree.remove(data)
}
})
}).catch(() => 0)
},
addRootMenu(row) {
this.dialog = true
this.selected = row
},
addMenu(row) {
this.dialog = true
this.form = {parentId: row.id}
this.selected = row
},
handleEdit(row) {
this.dialog = true
this.form = JSON.parse(JSON.stringify(row))
this.selected = row
},
handleSearch(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
computed: {
currentPage() {
const {hash} = this.$route
return hash == "#intro" ? IntroPage : List
}
},
created() {
this.getData()
this.dict.load("yesOrNo", "menuType")
this.dict.load("menuType", "yesOrNo")
}
}
</script>
@@ -177,85 +31,5 @@ export default {
<style lang="scss" scoped>
.AppMenuManager {
height: 100%;
::v-deep .ai-list__content--right-wrapper {
height: 100%;
display: flex;
flex-direction: column;
.el-tree {
width: 100%;
height: 100%;
font-size: 14px;
.menuItem {
flex: 1;
min-width: 0;
}
.el-tree-node__content {
border-bottom: 1px solid #d0d4dc;
}
}
.el-scrollbar {
flex: 1;
min-height: 0;
.el-scrollbar__wrap {
overflow-x: auto;
}
}
.headerRow {
background: #f3f4f5;
color: #666;
font-weight: bold;
align-items: center;
height: 40px;
.menuName {
padding-left: 16px;
}
}
.info {
gap: 16px;
text-align: center;
.showIndex, .status, .type, .style {
width: 80px;
}
.component {
width: 300px;
}
}
.operation {
width: 200px;
flex-shrink: 0;
justify-content: flex-end;
text-align: center;
.opBtn {
cursor: pointer;
width: 60px;
font-size: 14px;
color: #26f;
&.del {
color: #f46;
}
}
}
.menuName {
flex: 1;
min-width: 0;
}
}
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<section class="introPage">
<ai-detail :list="!edit">
<ai-title slot="title" title="引导页配置" isShowBottomBorder isShowBack @onBackClick="$router.push({})">
<template #rightBtn>
<ai-edit-btn @edit="edit=true,getConfigs()" @cancel="edit=false" @submit="submit"/>
</template>
</ai-title>
<template #content>
<el-form v-if="edit" :model="form" ref="IntroForm" size="small" :rules="rules" label-width="120px">
<ai-card title="基本信息">
<template #content>
<el-form-item label="副标题" prop="subtitle">
<el-input v-model="form.subtitle" clearable placeholder="请输入"/>
</el-form-item>
<el-form-item label="操作示例链接" prop="operationExamples">
<el-input v-model="form.operationExamples" clearable placeholder="请输入"/>
</el-form-item>
</template>
</ai-card>
<ai-card title="引导内容">
<template #content>
<el-form-item label-width="0" prop="guideContent">
<ai-editor :instance="instance" v-model="form.guideContent" placeholder="请输入" action="/oms/api/file/add" :params="{withoutToken:true}"/>
</el-form-item>
</template>
</ai-card>
</el-form>
<ai-intro v-else :id="$route.query.id" v-bind="$props"/>
</template>
</ai-detail>
</section>
</template>
<script>
import AiEditBtn from "../../components/AiEditBtn";
export default {
name: "introPage",
components: {AiEditBtn},
props: {
instance: Function,
dict: {default: () => ({})}
},
data() {
return {
form: {},
rules: {
subtitle: {required: true, message: "请输入副标题"},
guideContent: {required: true, message: "请输入引导内容"},
},
edit: false
}
},
methods: {
getConfigs() {
const {id} = this.$route.query
this.instance.post("/admin/sysappguideconfig/queryDetailById", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.form = res.data
}
})
},
submit(cb) {
this.$refs.IntroForm.validate(v => {
if (v) {
const {form, $route: {query: {id}}} = this
this.instance.post("/admin/sysappguideconfig/addOrUpdate", {...form, id}).then(res => {
if (res?.code == 0) {
this.$message.success("提交成功!")
cb()
this.edit = false
}
})
}
})
}
}
}
</script>
<style lang="scss" scoped>
.introPage {
height: 100%;
::v-deep.ai-detail__content--wrapper {
min-height: 100%;
&.list {
padding-top: 0;
}
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<section class="list">
<ai-list>
<ai-title slot="title" title="菜单配置" isShowBottomBorder/>
<template #content>
<ai-search-bar>
<template #left>
<el-button type="primary" icon="el-icon-circle-plus" @click="addRootMenu">添加一级目录</el-button>
</template>
<template #right>
<el-input size="small" v-model="search" clearable @change="$refs.MenuTree.filter(search)"
placeholder="菜单名称"/>
<el-button icon="iconfont iconResetting" @click="getData">刷新</el-button>
</template>
</ai-search-bar>
<el-row type="flex" class="headerRow">
<div class="menuName" v-text="`菜单名称`"/>
<el-row type="flex" align="middle" class="info">
<div class="style" v-text="`图标`"/>
<div class="type" v-text="`菜单类型`"/>
<div class="component" v-text="`应用模块`"/>
<div class="status" v-text="`是否显示`"/>
<div class="showIndex" v-text="`排序`"/>
</el-row>
<div class="operation" v-text="`操作`"/>
</el-row>
<el-scrollbar>
<el-tree ref="MenuTree" :data="treeData" :props="{children:'subSet'}" highlight-current node-key="id"
:filter-node-method="handleSearch">
<el-row type="flex" align="middle" slot-scope="{node,data}" class="menuItem">
<div class="menuName" v-text="data.name"/>
<el-row type="flex" align="middle" class="info">
<div class="style" :class="data.style"/>
<div class="type" v-text="dict.getLabel('menuType',data.type)"/>
<div class="component" v-text="data.component"/>
<div class="status" v-text="dict.getLabel('yesOrNo',data.status)"/>
<div class="showIndex" v-text="data.showIndex"/>
</el-row>
<el-row type="flex" align="middle" class="operation">
<div v-if="node.isLeaf" class="opBtn del" v-text="`删除`" @click="handleDelete(data)"/>
<div v-if="data.component&&data.type==1" class="opBtn" v-text="`引导页`" @click="$router.push({hash:'#intro',query:{id:data.id}})"/>
<div v-if="data.type<2" class="opBtn" v-text="`添加下级`" @click="addMenu(data)"/>
<div class="opBtn" v-text="`编辑`" @click="handleEdit(data)"/>
</el-row>
</el-row>
</el-tree>
</el-scrollbar>
</template>
</ai-list>
<ai-dialog :visible.sync="dialog" title="菜单设置" width="500px" @onConfirm="handleSubmit"
@closed="form={},selected={}">
<el-form ref="MenuForm" :model="form" size="small" label-width="100px" :rules="rules">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" clearable/>
</el-form-item>
<el-form-item label="菜单类型" prop="type">
<ai-select v-model="form.type" clearable :selectList="dict.getDict('menuType')"/>
</el-form-item>
<template v-if="form.type==0">
<el-form-item label="菜单图标" prop="style">
<el-input v-model="form.style" placeholder="请输入" clearable/>
</el-form-item>
</template>
<template v-if="form.type==1">
<el-form-item label="路由名" prop="route">
<span v-text="form.route||'提交保存后会自动生成'"/>
</el-form-item>
<el-form-item label="菜单应用" prop="component">
<el-input v-model="form.component" placeholder="请输入" clearable/>
</el-form-item>
<el-form-item label="路径(path)" prop="path">
<el-input v-model="form.path" placeholder="请输入" clearable/>
</el-form-item>
</template>
<template v-if="form.type==2">
<el-form-item label="权限码" prop="permission">
<el-input v-model="form.permission" placeholder="请输入" clearable/>
</el-form-item>
</template>
<el-form-item label="显示菜单" prop="status">
<ai-select v-model="form.status" clearable :selectList="dict.getDict('yesOrNo')"/>
</el-form-item>
<el-form-item v-if="form.type<2" label="排序" prop="showIndex">
<el-input v-model="form.showIndex" placeholder="请输入" clearable/>
</el-form-item>
</el-form>
</ai-dialog>
</section>
</template>
<script>
export default {
name: "list",
props: {
instance: Function,
dict: {default: () => ({})}
},
data() {
return {
treeData: [],
dialog: false,
form: {},
selected: {},
rules: {
name: [{required: true, message: "请输入 菜单名称"}],
type: [{required: true, message: "请选择 菜单类型"}],
status: [{required: true, message: "请选择 显示菜单"}],
showIndex: [{required: true, message: "请输入 排序"}],
permission: [{required: true, message: "请输入 权限码"}],
},
search: ""
}
},
methods: {
getData() {
return this.instance.post("/admin/menu/menuTree").then(res => {
if (res?.data) {
this.treeData = res.data
}
})
},
handleSubmit() {
this.$refs.MenuForm.validate(v => {
if (v) {
this.instance.post("/admin/menu/addOrUpdate", this.form).then(res => {
if (res?.code == 0) {
this.$message.success("提交成功!")
this.dialog = false
if (!!this.form.id) {
let node = this.$refs.MenuTree.getNode(this.form)
node.data = this.form
} else if (!!this.form.parentId) {
this.$refs.MenuTree.append(this.form, this.selected)
} else this.getData()
}
})
}
})
},
handleDelete(data) {
let {id} = data
this.$confirm("是否要删除该菜单").then(() => {
this.instance.post("/admin/menu/delete", null, {
params: {id}
}).then(res => {
if (res?.code == 0) {
this.$message.success("删除成功!")
this.dialog = false
this.$refs.MenuTree.remove(data)
}
})
}).catch(() => 0)
},
addRootMenu(row) {
this.dialog = true
this.selected = row
},
addMenu(row) {
this.dialog = true
this.form = {parentId: row.id}
this.selected = row
},
handleEdit(row) {
this.dialog = true
this.form = JSON.parse(JSON.stringify(row))
this.selected = row
},
handleSearch(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
}
},
created() {
this.getData()
}
}
</script>
<style lang="scss" scoped>
.list {
height: 100%;
::v-deep .ai-list__content--right-wrapper {
height: calc(100% - 10px);
display: flex;
flex-direction: column;
.el-tree {
width: 100%;
height: 100%;
font-size: 14px;
.menuItem {
flex: 1;
min-width: 0;
}
.el-tree-node:nth-of-type(2n) {
background: rgba(#26f, .05);
}
.el-tree-node__content {
border-bottom: 1px solid #d0d4dc;
}
}
.el-scrollbar {
flex: 1;
min-height: 0;
.el-scrollbar__wrap {
overflow-x: auto;
}
}
.headerRow {
background: #f3f4f5;
color: #666;
font-weight: bold;
align-items: center;
height: 40px;
.menuName {
padding-left: 16px;
}
}
.info {
gap: 16px;
text-align: center;
.showIndex, .status, .type, .style {
width: 80px;
}
.component {
width: 300px;
}
}
.operation {
width: 300px;
flex-shrink: 0;
justify-content: flex-end;
text-align: center;
.opBtn {
cursor: pointer;
width: 60px;
font-size: 14px;
color: #26f;
&.del {
color: #f46;
}
}
}
.menuName {
flex: 1;
min-width: 0;
}
}
}
</style>

View File

@@ -1,178 +1,16 @@
<template>
<section class="AppQyWxConfig">
<ai-list>
<ai-title slot="title" title="企业微信配置" isShowBottomBorder/>
<template #content>
<ai-search-bar>
<template #left>
<el-button type="primary" @click="add" icon="iconfont iconAdd">新增</el-button>
</template>
<template #right>
<el-input size="small" placeholder="搜索名称" v-model="search.name" clearable
@clear="page.current = 1,search.name = '', getTableData()"
v-throttle="() => {page.current = 1, getTableData()}"/>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="page.total" :current.sync="page.current" :size.sync="page.size"
@getList="getTableData" :col-configs="colConfigs">
<el-table-column type="expand" slot="expand">
<template slot-scope="{row}">
<ai-wrapper>
<ai-info-item labelWidth="200px" v-for="op in desConfigs" :key="op.prop" :value="row[op.prop]"
v-bind="op"/>
</ai-wrapper>
</template>
</el-table-column>
<el-table-column slot="status" align="center" label="状态" width="150">
<template v-slot="{ row }">
<el-switch v-model="row.status" @change="onChange(row)" active-value="1" inactive-value="0"
active-color="#5088FF" inactive-color="#D0D4DC"></el-switch>
</template>
</el-table-column>
<el-table-column slot="miniappStatus" align="center" label="小程序状态" width="150">
<template v-slot="{ row }">
<el-switch v-model="row.miniappStatus" @change="onMiniappStatusChange(row)" active-value="1"
inactive-value="0"
active-color="#5088FF" inactive-color="#D0D4DC"></el-switch>
</template>
</el-table-column>
<el-table-column slot="options" align="center" label="操作" width="400">
<el-row type="flex" justify="center" align="middle" slot-scope="{row}">
<el-button type="text" @click="detail(row)">详情</el-button>
<el-button type="text" @click="del(row)">删除</el-button>
<el-button type="text" @click="handleSystemInfo(row.id)">系统信息</el-button>
<el-button type="text" @click="handlePush(row.id)">推送随手拍样式</el-button>
</el-row>
</el-table-column>
</ai-table>
</template>
</ai-list>
<ai-dialog title="新增" :visible.sync="dialog" width="800px" @onConfirm="confirm">
<el-form ref="form" :model="dialogForm" :rules="rules" size="small" label-width="180px">
<el-form-item required label="名称" prop="name">
<el-input v-model.trim="dialogForm.name" placeholder="请输入名称" show-word-limit maxlength="100"></el-input>
</el-form-item>
<el-form-item label="企业微信ID" prop="corpId">
<el-input v-model.trim="dialogForm.corpId" placeholder="请输入企业微信ID" show-word-limit maxlength="32"></el-input>
</el-form-item>
<el-form-item label="企业微信通讯录SECRET" prop="corpAddressBookSecret">
<el-input v-model.trim="dialogForm.corpAddressBookSecret" placeholder="请输入企业微信通讯录SECRET" show-word-limit
maxlength="64"></el-input>
</el-form-item>
<el-form-item label="企业微信AESKEY" prop="corpAeskey">
<el-input v-model.trim="dialogForm.corpAeskey" placeholder="请输入企业微信AESKEY" show-word-limit
maxlength="64"></el-input>
</el-form-item>
<el-form-item label="企业微信AGENTID" prop="corpAgentId">
<el-input v-model.trim="dialogForm.corpAgentId" placeholder="请输入企业微信AGENTID" show-word-limit
maxlength="100"></el-input>
</el-form-item>
<el-form-item label="随手拍AGENTID" prop="clapAgentId">
<el-input v-model.trim="dialogForm.clapAgentId" placeholder="请输入随手拍AGENTID" show-word-limit
maxlength="100"></el-input>
</el-form-item>
<el-form-item label="企业微信SECRET" prop="corpSecret">
<el-input v-model.trim="dialogForm.corpSecret" placeholder="请输入企业微信SECRET" show-word-limit
maxlength="255"></el-input>
</el-form-item>
<el-form-item label="企业微信TOKEN" prop="corpToken">
<el-input v-model.trim="dialogForm.corpToken" placeholder="请输入企业微信TOKEN" show-word-limit
maxlength="32"></el-input>
</el-form-item>
<el-form-item label="小程序APPID" prop="miniappAppid">
<el-input v-model.trim="dialogForm.miniappAppid" placeholder="请输入小程序APPID" show-word-limit
maxlength="32"></el-input>
</el-form-item>
<el-form-item label="小程序SECRET" prop="miniappSecret">
<el-input v-model.trim="dialogForm.miniappSecret" placeholder="请输入小程序SECRET" show-word-limit
maxlength="32"></el-input>
</el-form-item>
<el-form-item label="企微访问域名" prop="dvcpUrl">
<el-input v-model.trim="dialogForm.dvcpUrl" placeholder="请输入企微访问域名" show-word-limit maxlength="128">
<template slot="prepend">Http://</template>
</el-input>
</el-form-item>
<el-form-item label="web访问域名" prop="webUrl">
<el-input v-model.trim="dialogForm.webUrl" placeholder="请输入web访问域名" show-word-limit maxlength="128">
<template slot="prepend">Http://</template>
</el-input>
</el-form-item>
<el-form-item label="地区" prop="areaId">
<ai-area-select :instance="instance" v-model="dialogForm.areaId" alwaysShow
@name="(e)=>dialogForm.areaName=e"/>
</el-form-item>
<el-form-item label="地图中心点" prop="lat">
<el-button type="primary" icon="iconfont iconAdd" @click="showMap=true">设置地点</el-button>
<div v-if="dialogForm.lat">{{ dialogForm.address }}</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model.trim="dialogForm.status">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="小程序状态" prop="miniappStatus">
<el-radio-group v-model.trim="dialogForm.miniappStatus">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="系统配置信息" prop="systemInfo">
<el-input type="textarea" :rows="2" placeholder="请输入系统配置信息" v-model="dialogForm.systemInfo"
maxlength="50000"/>
</el-form-item>
</el-form>
</ai-dialog>
<ai-dialog title="地图" :visible.sync="showMap" @opened="initMap" width="800px" class="mapDialog"
@onConfirm="selectMap">
<div id="map"></div>
<el-input id="searchPlaceInput" size="medium" class="searchPlaceInput" clearable v-model="searchPlace"
autocomplete="on"
@change="placeSearch.search(searchPlace)">
<el-button type="primary" slot="append" @click="placeSearch.search(searchPlace)">搜索</el-button>
</el-input>
<div id="searchPlaceOutput"/>
</ai-dialog>
<ai-dialog title="系统信息设置" :visible.sync="sysInfoDialog" width="600px" @onConfirm="submitSystemInfo"
@closed="sysInfo={}">
<el-form size="small" label-width="140px">
<el-form-item label="页签标题">
<el-input v-model="sysInfo.title" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="系统标题">
<el-input v-model="sysInfo.fullTitle" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="logo">
<el-input v-model="sysInfo.logo" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="登录页左上角标题">
<el-input v-model="sysInfo.name" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="登录页副标题">
<el-input type="textarea" rows="5" v-model="sysInfo.desc" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="版权所有">
<el-input v-model="sysInfo.recordDesc" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="备案号">
<el-input v-model="sysInfo.recordNo" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="备案跳转链接">
<el-input v-model="sysInfo.recordURL" placeholder="请输入..." clearable/>
</el-form-item>
</el-form>
</ai-dialog>
<component :is="currentPage" v-bind="$props"/>
</section>
</template>
<script>
import {mapState} from "vuex";
import AMapLoader from "@amap/amap-jsapi-loader"
import List from "./list";
import ThemeSetting from "./themeSetting";
export default {
name: "AppQyWxConfig",
components: {ThemeSetting, List},
label: "企业微信配置",
props: {
instance: Function,
@@ -180,245 +18,13 @@ export default {
permissions: Function
},
computed: {
...mapState(['user']),
colConfigs() {
return [
{slot: 'expand'},
{prop: "name", label: "名称"},
{prop: "corpId", label: "企业微信ID", width: 180},
{slot: "status",},
{slot: "miniappStatus"},
{prop: "createTime", label: "创建时间"},
{slot: "options"},
]
},
desConfigs() {
let isLine = true
return [
{prop: "corpAddressBookSecret", label: "企业微信通讯录SECRET", width: 200},
{prop: "corpAgentId", label: "企业微信AGENTID", width: 150},
{prop: "corpSecret", label: "企业微信SECRET", isLine},
{prop: "corpToken", label: "企业微信TOKEN", width: 150},
{prop: "corpAeskey", label: "企业微信AESKEY", width: 150},
{prop: "miniappAppid", label: "小程序APPID", width: 150},
{prop: "miniappSecret", label: "小程序SECRET", width: 150},
{prop: "areaId", label: "地区编码", width: 150, isLine},
{prop: "lat", label: "纬度", width: 100},
{prop: "lng", label: "经度", width: 100},
{prop: "address", label: "中心点", width: 100, isLine},
{prop: "webUrl", label: "管理端地址", width: 100},
{prop: "dvcpUrl", label: "企微端地址", width: 100},
]
},
rules() {
return {
name: [{required: true, message: "请填写名称"}],
corpId: [{required: true, message: "请填写企业微信ID"}],
corpAddressBookSecret: [{required: true, message: "请填写企业微信通讯录SECRET"}],
corpAeskey: [{required: true, message: "请填写企业微信AESKEY"}],
corpAgentId: [{required: true, message: "请填写企业微信AGENTID"}],
corpSecret: [{required: true, message: "请填写企业微信SECRET"}],
corpToken: [{required: true, message: "请填写企业微信TOKEN"}],
miniappAppid: [{required: true, message: "请填写小程序APPID"}],
miniappSecret: [{required: true, message: "请填写小程序SECRET"}],
dvcpUrl: [{required: true, message: "请填写企微访问域名"}],
webUrl: [{required: true, message: "请填写web访问域名"}],
status: [{required: true, message: "请选择状态", trigger: "change"}],
miniappStatus: [{required: true, message: "请选择小程序状态", trigger: "change"}],
areaId: [{required: true, message: "请选择地区", trigger: "change"}],
lat: [{required: true, message: "请选择中心点"}],
systemInfo: [{required: true, message: "请输入系统配置信息"}],
}
},
},
data() {
return {
page: {current: 1, size: 10, total: 0},
dialog: false,
showMap: false,
map: null,
placeSearch: null,
placeDetail: {},
searchPlace: "",
dialogForm: {},
tableData: [],
search: {
name: ""
},
sysInfo: {},
sysInfoDialog: false
}
},
methods: {
selectMap() {
Object.keys(this.placeDetail).map(e => this.dialogForm[e] = this.placeDetail[e]);
this.showMap = false;
},
initMap() {
AMapLoader.load({
key: 'b553334ba34f7ac3cd09df9bc8b539dc',
version: '2.0',
plugins: ['AMap.PlaceSearch', 'AMap.AutoComplete', 'AMap.Geocoder'],
}).then(AMap => {
this.map = new AMap.Map('map', {
resizeEnable: true,
zooms: [6, 20],
center: [116.394681, 39.910283],
zoom: 11
})
this.placeSearch = new AMap.PlaceSearch({map: this.map})
new AMap.AutoComplete({
input: "searchPlaceInput",
output: 'searchPlaceOutput',
}).on('select', e => {
if (e?.poi) {
this.placeSearch.setCity(e.poi.adcode);
this.movePosition(e.poi.location)
}
})
this.map.on('click', e => {
new AMap.Geocoder().getAddress(e.lnglat, (sta, res) => {
if (res?.regeocode) {
this.placeDetail = {
lng: e.lnglat?.lng,
lat: e.lnglat?.lat,
address: res.regeocode.formattedAddress
}
}
})
this.movePosition(e.lnglat)
})
})
},
movePosition(center) {
if (this.map) {
this.map.clearMap()
this.map.panTo(center)
this.map.add([
new AMap.Marker({
position: center,
clickable: true
})
])
this.map.setFitView()
}
},
onChange(row) {
this.instance.post(`/app/appdvcpconfig/setStatus`, null, {
params: {
id: row.id,
status: row.status
}
}).then((res) => {
if (res.code == 0) {
this.$message.success(+row.status ? '已启用' : '已禁用');
this.getTableData();
}
})
},
onMiniappStatusChange(row) {
this.instance.post(`/app/appdvcpconfig/setMiniappStatus`, null, {
params: {
id: row.id,
status: row.miniappStatus
}
}).then((res) => {
if (res.code == 0) {
this.$message.success(+row.miniappStatus ? '已启用' : '已禁用');
this.getTableData();
}
})
},
add() {
this.dialogForm = {};
this.dialog = true;
},
del(row) {
this.$confirm("是否要删除?").then(_ => {
this.instance.post("/app/appdvcpconfig/delete", null, {
params: {
ids: row.id
}
}).then(res => {
if (res.code == 0) {
this.$message.success("删除成功");
this.dialog = false;
this.getTableData();
}
})
})
},
detail(row) {
this.instance.post("/app/appdvcpconfig/detail", null, {
params: {
id: row.id
}
}).then(res => {
if (res && res.data) {
this.dialogForm = {...res.data};
this.dialog = true;
}
})
},
getTableData() {
this.instance.post("/app/appdvcpconfig/list", null, {
params: {...this.page, ...this.search}
}).then(res => {
if (res?.data) {
this.tableData = res.data?.records
this.page.total = res.data.total
}
})
},
confirm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.instance.post("/app/appdvcpconfig/addOrUpdate", {
...this.dialogForm,
}).then(res => {
if (res.code == 0) {
this.$message.success(this.dialogForm.id ? "修改成功" : "新增成功");
this.dialog = false;
this.getTableData();
}
})
}
})
},
handleSystemInfo(id) {
this.sysInfoDialog = true
this.getSystemInfo(id)
},
getSystemInfo(id) {
this.instance.post("/app/appdvcpconfig/getSystemInfo", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.sysInfo = JSON.parse(res.data)
this.sysInfo.id = id
}
})
},
submitSystemInfo() {
let {id} = this.sysInfo
this.instance.post("/app/appdvcpconfig/updateSystemInfo", this.sysInfo, {params: {id}}).then(res => {
if (res?.code == 0) {
this.$message.success("提交成功!")
this.sysInfoDialog = false
}
})
},
handlePush(id) {
this.instance.post("/app/appclapeventinfo/setAppWorkbench", null, {params: {id}}).then(res => {
if (res?.code == 0) {
this.$message.success("推送成功!")
}
})
currentPage() {
const {hash} = this.$route
return hash == "#theme" ? ThemeSetting : List
}
},
created() {
this.dict.load("integralRuleStatus").then(this.getTableData);
this.dict.load("yesOrNo", "themeWeb", 'themeMp', "themeWxwork")
}
}
</script>
@@ -426,54 +32,5 @@ export default {
<style lang="scss" scoped>
.AppQyWxConfig {
height: 100%;
::v-deep .mapDialog {
.el-dialog__body {
padding: 0;
.ai-dialog__content {
padding: 0;
}
.ai-dialog__content--wrapper {
padding: 0 !important;
position: relative;
}
#map {
width: 100%;
height: 500px;
}
.searchPlaceInput {
position: absolute;
width: 250px;
top: 30px;
left: 25px;
}
#searchPlaceOutput {
position: absolute;
width: 250px;
left: 25px;
height: initial;
top: 80px;
background: white;
z-index: 250;
max-height: 300px;
overflow-y: auto;
.auto-item {
text-align: left;
font-size: 14px;
padding: 8px;
box-sizing: border-box;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<section class="AiSelectCard" flex>
<div class="checkCard" v-for="op in list" :key="op[props.value]" :class="{checked:op.dictValue==value}">
<el-image :src="op.thumb"/>
<el-row type="flex" class="bottomPane">
<b class="label fill" v-text="op[props.label]"/>
<el-button type="text" @click.stop="$emit('change',op[props.value])">使用</el-button>
</el-row>
</div>
</section>
</template>
<script>
export default {
name: "AiSelectCard",
model: {
prop: "value",
event: "change"
},
props: {
value: {default: ""},
ops: {default: () => []},
type: {default: "web"},
dict: String,
prop: {default: () => ({})}
},
computed: {
list: v => (v.dict ? v.$dict.getDict(v.dict) : v.ops || []).map(e => ({
...e,
thumb: `https://cdn.cunwuyun.cn/theme/thumb/${v.type}_${e[v.props.value]}.png`
})),
props: v => ({value: 'dictValue', label: 'dictName', ...v.prop})
}
}
</script>
<style lang="scss" scoped>
.AiSelectCard {
display: flex;
flex-wrap: wrap;
gap: 16px;
.checkCard {
width: 360px;
background: #fff;
border-radius: 8px;
overflow: hidden;
position: relative;
.label {
user-select: none;
}
&.checked:before {
position: absolute;
content: "应用中";
top: 16px;
left: 16px;
z-index: 9;
padding: 8px 16px;
background: rgba(#000, .7);
color: #fff;
border-radius: 8px;
}
.bottomPane {
height: 48px;
align-items: center;
padding: 0 16px;
border-top: 1px solid #eee;
}
}
}
</style>

View File

@@ -0,0 +1,485 @@
<template>
<section class="list">
<ai-list>
<ai-title slot="title" title="企业微信配置" isShowBottomBorder/>
<template #content>
<ai-search-bar>
<template #left>
<el-button type="primary" @click="add" icon="iconfont iconAdd">新增</el-button>
</template>
<template #right>
<el-input size="small" placeholder="搜索名称" v-model="search.name" clearable
@clear="page.current = 1,search.name = '', getTableData()"
v-throttle="() => {page.current = 1, getTableData()}"/>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="page.total" :current.sync="page.current" :size.sync="page.size"
@getList="getTableData" :col-configs="colConfigs">
<el-table-column type="expand" slot="expand">
<template slot-scope="{row}">
<ai-wrapper>
<ai-info-item labelWidth="200px" v-for="op in desConfigs" :key="op.prop" :value="row[op.prop]"
v-bind="op"/>
</ai-wrapper>
</template>
</el-table-column>
<el-table-column slot="status" align="center" label="状态" width="150">
<template v-slot="{ row }">
<el-switch v-model="row.status" @change="onChange(row)" active-value="1" inactive-value="0"
active-color="#5088FF" inactive-color="#D0D4DC"></el-switch>
</template>
</el-table-column>
<el-table-column slot="miniappStatus" align="center" label="小程序状态" width="150">
<template v-slot="{ row }">
<el-switch v-model="row.miniappStatus" @change="onMiniappStatusChange(row)" active-value="1"
inactive-value="0"
active-color="#5088FF" inactive-color="#D0D4DC"></el-switch>
</template>
</el-table-column>
<el-table-column slot="options" align="center" label="操作" width="400">
<el-row type="flex" justify="center" align="middle" slot-scope="{row}">
<el-button type="text" @click="detail(row)">详情</el-button>
<el-button type="text" @click="del(row)">删除</el-button>
<el-button type="text" @click="handleSystemInfo(row.id)">系统信息</el-button>
<el-button type="text" @click="handlePush(row.id)">推送随手拍样式</el-button>
<el-button type="text" @click="handleTheme(row.id)">主题样式</el-button>
</el-row>
</el-table-column>
</ai-table>
</template>
</ai-list>
<ai-dialog title="新增" :visible.sync="dialog" width="800px" @onConfirm="confirm">
<el-form ref="form" :model="dialogForm" :rules="rules" size="small" label-width="180px">
<el-form-item required label="名称" prop="name">
<el-input v-model.trim="dialogForm.name" placeholder="请输入名称" show-word-limit maxlength="100"></el-input>
</el-form-item>
<el-form-item label="企业微信ID" prop="corpId">
<el-input v-model.trim="dialogForm.corpId" placeholder="请输入企业微信ID" show-word-limit maxlength="32"></el-input>
</el-form-item>
<el-form-item label="企业微信通讯录SECRET" prop="corpAddressBookSecret">
<el-input v-model.trim="dialogForm.corpAddressBookSecret" placeholder="请输入企业微信通讯录SECRET" show-word-limit
maxlength="64"></el-input>
</el-form-item>
<el-form-item label="企业微信AESKEY" prop="corpAeskey">
<el-input v-model.trim="dialogForm.corpAeskey" placeholder="请输入企业微信AESKEY" show-word-limit
maxlength="64"></el-input>
</el-form-item>
<el-form-item label="企业微信AGENTID" prop="corpAgentId">
<el-input v-model.trim="dialogForm.corpAgentId" placeholder="请输入企业微信AGENTID" show-word-limit
maxlength="100"></el-input>
</el-form-item>
<el-form-item label="随手拍AGENTID" prop="clapAgentId">
<el-input v-model.trim="dialogForm.clapAgentId" placeholder="请输入随手拍AGENTID" show-word-limit
maxlength="100"></el-input>
</el-form-item>
<el-form-item label="企业微信SECRET" prop="corpSecret">
<el-input v-model.trim="dialogForm.corpSecret" placeholder="请输入企业微信SECRET" show-word-limit
maxlength="255"></el-input>
</el-form-item>
<el-form-item label="企业微信TOKEN" prop="corpToken">
<el-input v-model.trim="dialogForm.corpToken" placeholder="请输入企业微信TOKEN" show-word-limit
maxlength="32"></el-input>
</el-form-item>
<el-form-item label="小程序APPID" prop="miniappAppid">
<el-input v-model.trim="dialogForm.miniappAppid" placeholder="请输入小程序APPID" show-word-limit
maxlength="32"></el-input>
</el-form-item>
<el-form-item label="小程序SECRET" prop="miniappSecret">
<el-input v-model.trim="dialogForm.miniappSecret" placeholder="请输入小程序SECRET" show-word-limit
maxlength="32"></el-input>
</el-form-item>
<el-form-item label="企微访问域名" prop="dvcpUrl">
<el-input v-model.trim="dialogForm.dvcpUrl" placeholder="请输入企微访问域名" show-word-limit maxlength="128">
<template slot="prepend">Http://</template>
</el-input>
</el-form-item>
<el-form-item label="web访问域名" prop="webUrl">
<el-input v-model.trim="dialogForm.webUrl" placeholder="请输入web访问域名" show-word-limit maxlength="128">
<template slot="prepend">Http://</template>
</el-input>
</el-form-item>
<el-form-item label="地区" prop="areaId">
<ai-area-select :instance="instance" v-model="dialogForm.areaId" alwaysShow
@name="(e)=>dialogForm.areaName=e"/>
</el-form-item>
<el-form-item label="地图中心点" prop="lat">
<el-button type="primary" icon="iconfont iconAdd" @click="showMap=true">设置地点</el-button>
<div v-if="dialogForm.lat">{{ dialogForm.address }}</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model.trim="dialogForm.status">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="小程序状态" prop="miniappStatus">
<el-radio-group v-model.trim="dialogForm.miniappStatus">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="系统配置信息" prop="systemInfo">
<el-input type="textarea" :rows="2" placeholder="请输入系统配置信息" v-model="dialogForm.systemInfo"
maxlength="50000"/>
</el-form-item>
</el-form>
</ai-dialog>
<ai-dialog title="地图" :visible.sync="showMap" @opened="initMap" width="800px" class="mapDialog"
@onConfirm="selectMap">
<div id="map"></div>
<el-input id="searchPlaceInput" size="medium" class="searchPlaceInput" clearable v-model="searchPlace"
autocomplete="on"
@change="placeSearch.search(searchPlace)">
<el-button type="primary" slot="append" @click="placeSearch.search(searchPlace)">搜索</el-button>
</el-input>
<div id="searchPlaceOutput"/>
</ai-dialog>
<ai-dialog title="系统信息设置" :visible.sync="sysInfoDialog" width="600px" @onConfirm="submitSystemInfo"
@closed="sysInfo={}">
<el-form size="small" label-width="140px">
<el-form-item label="页签标题">
<el-input v-model="sysInfo.title" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="系统标题">
<el-input v-model="sysInfo.fullTitle" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="logo">
<el-input v-model="sysInfo.logo" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="登录页左上角标题">
<el-input v-model="sysInfo.name" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="登录页副标题">
<el-input type="textarea" rows="5" v-model="sysInfo.desc" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="版权所有">
<el-input v-model="sysInfo.recordDesc" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="备案号">
<el-input v-model="sysInfo.recordNo" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="备案跳转链接">
<el-input v-model="sysInfo.recordURL" placeholder="请输入..." clearable/>
</el-form-item>
<el-form-item label="框架版本">
<!--edition 版本标准版standard上架版saas-->
<el-input v-model="sysInfo.edition" placeholder="请输入..." clearable/>
</el-form-item>
</el-form>
</ai-dialog>
</section>
</template>
<script>
import {mapState} from "vuex";
import AMapLoader from "@amap/amap-jsapi-loader"
export default {
name: "list",
props: {
instance: Function,
dict: Object,
permissions: Function
},
computed: {
...mapState(['user']),
colConfigs() {
return [
{slot: 'expand'},
{prop: "name", label: "名称"},
{prop: "corpId", label: "企业微信ID", width: 180},
{slot: "status",},
{slot: "miniappStatus"},
{prop: "createTime", label: "创建时间"},
{slot: "options"},
]
},
desConfigs() {
let isLine = true
return [
{prop: "corpAddressBookSecret", label: "企业微信通讯录SECRET", width: 200},
{prop: "corpAgentId", label: "企业微信AGENTID", width: 150},
{prop: "corpSecret", label: "企业微信SECRET", isLine},
{prop: "corpToken", label: "企业微信TOKEN", width: 150},
{prop: "corpAeskey", label: "企业微信AESKEY", width: 150},
{prop: "miniappAppid", label: "小程序APPID", width: 150},
{prop: "miniappSecret", label: "小程序SECRET", width: 150},
{prop: "areaId", label: "地区编码", width: 150, isLine},
{prop: "lat", label: "纬度", width: 100},
{prop: "lng", label: "经度", width: 100},
{prop: "address", label: "中心点", width: 100, isLine},
{prop: "webUrl", label: "管理端地址", width: 100},
{prop: "dvcpUrl", label: "企微端地址", width: 100},
]
},
rules() {
return {
name: [{required: true, message: "请填写名称"}],
corpId: [{required: true, message: "请填写企业微信ID"}],
corpAddressBookSecret: [{required: true, message: "请填写企业微信通讯录SECRET"}],
corpAeskey: [{required: true, message: "请填写企业微信AESKEY"}],
corpAgentId: [{required: true, message: "请填写企业微信AGENTID"}],
corpSecret: [{required: true, message: "请填写企业微信SECRET"}],
corpToken: [{required: true, message: "请填写企业微信TOKEN"}],
miniappAppid: [{required: true, message: "请填写小程序APPID"}],
miniappSecret: [{required: true, message: "请填写小程序SECRET"}],
dvcpUrl: [{required: true, message: "请填写企微访问域名"}],
webUrl: [{required: true, message: "请填写web访问域名"}],
status: [{required: true, message: "请选择状态", trigger: "change"}],
miniappStatus: [{required: true, message: "请选择小程序状态", trigger: "change"}],
areaId: [{required: true, message: "请选择地区", trigger: "change"}],
lat: [{required: true, message: "请选择中心点"}],
systemInfo: [{required: true, message: "请输入系统配置信息"}],
}
},
},
data() {
return {
page: {current: 1, size: 10, total: 0},
dialog: false,
showMap: false,
map: null,
placeSearch: null,
placeDetail: {},
searchPlace: "",
dialogForm: {},
tableData: [],
search: {
name: ""
},
sysInfo: {},
sysInfoDialog: false
}
},
methods: {
selectMap() {
Object.keys(this.placeDetail).map(e => this.dialogForm[e] = this.placeDetail[e]);
this.showMap = false;
},
initMap() {
AMapLoader.load({
key: 'b553334ba34f7ac3cd09df9bc8b539dc',
version: '2.0',
plugins: ['AMap.PlaceSearch', 'AMap.AutoComplete', 'AMap.Geocoder'],
}).then(AMap => {
this.map = new AMap.Map('map', {
resizeEnable: true,
zooms: [6, 20],
center: [116.394681, 39.910283],
zoom: 11
})
this.placeSearch = new AMap.PlaceSearch({map: this.map})
new AMap.AutoComplete({
input: "searchPlaceInput",
output: 'searchPlaceOutput',
}).on('select', e => {
if (e?.poi) {
this.placeSearch.setCity(e.poi.adcode);
this.movePosition(e.poi.location)
}
})
this.map.on('click', e => {
new AMap.Geocoder().getAddress(e.lnglat, (sta, res) => {
if (res?.regeocode) {
this.placeDetail = {
lng: e.lnglat?.lng,
lat: e.lnglat?.lat,
address: res.regeocode.formattedAddress
}
}
})
this.movePosition(e.lnglat)
})
})
},
movePosition(center) {
if (this.map) {
this.map.clearMap()
this.map.panTo(center)
this.map.add([
new AMap.Marker({
position: center,
clickable: true
})
])
this.map.setFitView()
}
},
onChange(row) {
this.instance.post(`/app/appdvcpconfig/setStatus`, null, {
params: {
id: row.id,
status: row.status
}
}).then((res) => {
if (res.code == 0) {
this.$message.success(+row.status ? '已启用' : '已禁用');
this.getTableData();
}
})
},
onMiniappStatusChange(row) {
this.instance.post(`/app/appdvcpconfig/setMiniappStatus`, null, {
params: {
id: row.id,
status: row.miniappStatus
}
}).then((res) => {
if (res.code == 0) {
this.$message.success(+row.miniappStatus ? '已启用' : '已禁用');
this.getTableData();
}
})
},
add() {
this.dialogForm = {};
this.dialog = true;
},
del(row) {
this.$confirm("是否要删除?").then(_ => {
this.instance.post("/app/appdvcpconfig/delete", null, {
params: {
ids: row.id
}
}).then(res => {
if (res.code == 0) {
this.$message.success("删除成功");
this.dialog = false;
this.getTableData();
}
})
})
},
detail(row) {
this.instance.post("/app/appdvcpconfig/detail", null, {
params: {
id: row.id
}
}).then(res => {
if (res && res.data) {
this.dialogForm = {...res.data};
this.dialog = true;
}
})
},
getTableData() {
this.instance.post("/app/appdvcpconfig/list", null, {
params: {...this.page, ...this.search}
}).then(res => {
if (res?.data) {
this.tableData = res.data?.records
this.page.total = res.data.total
}
})
},
confirm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.instance.post("/app/appdvcpconfig/addOrUpdate", {
...this.dialogForm,
}).then(res => {
if (res.code == 0) {
this.$message.success(this.dialogForm.id ? "修改成功" : "新增成功");
this.dialog = false;
this.getTableData();
}
})
}
})
},
handleSystemInfo(id) {
this.sysInfoDialog = true
this.getSystemInfo(id)
},
getSystemInfo(id) {
this.instance.post("/app/appdvcpconfig/getSystemInfo", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.sysInfo = JSON.parse(res.data)
this.sysInfo.id = id
}
})
},
submitSystemInfo() {
let {id} = this.sysInfo
this.instance.post("/app/appdvcpconfig/updateSystemInfo", this.sysInfo, {params: {id}}).then(res => {
if (res?.code == 0) {
this.$message.success("提交成功!")
this.sysInfoDialog = false
}
})
},
handlePush(id) {
this.instance.post("/app/appclapeventinfo/setAppWorkbench", null, {params: {id}}).then(res => {
if (res?.code == 0) {
this.$message.success("推送成功!")
}
})
},
handleTheme(id) {
this.$router.push({hash: "#theme", query: {id}})
}
},
created() {
this.getTableData()
}
}
</script>
<style lang="scss" scoped>
.list {
height: 100%;
::v-deep .mapDialog {
.el-dialog__body {
padding: 0;
.ai-dialog__content {
padding: 0;
}
.ai-dialog__content--wrapper {
padding: 0 !important;
position: relative;
}
#map {
width: 100%;
height: 500px;
}
.searchPlaceInput {
position: absolute;
width: 250px;
top: 30px;
left: 25px;
}
#searchPlaceOutput {
position: absolute;
width: 250px;
left: 25px;
height: initial;
top: 80px;
background: white;
z-index: 250;
max-height: 300px;
overflow-y: auto;
.auto-item {
text-align: left;
font-size: 14px;
padding: 8px;
box-sizing: border-box;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<section class="themeSetting">
<ai-detail list>
<ai-title slot="title" title="主题样式" isShowBottomBorder isShowBack @back="cancel">
<template #rightBtn>
<span class="mar-r8" v-text="'灰色滤镜'"/>
<el-switch size="mini" v-model="form.enableGreyFilter" name="灰色滤镜" border active-value="1" inactive-value="0"/>
</template>
</ai-title>
<template #content>
<el-form size="small" :model="form" ref="ThemeForm">
<ai-title title="WEB后台"/>
<ai-select-card dict="themeWeb" v-model="form.colorScheme.web"/>
</el-form>
</template>
<template #footer>
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submit">提交</el-button>
</template>
</ai-detail>
</section>
</template>
<script>
import AiSelectCard from "./components/AiSelectCard";
export default {
name: "themeSetting",
components: {AiSelectCard},
props: {
instance: Function,
dict: Object,
permissions: Function
},
data() {
return {
form: {enableGreyFilter: '0', colorScheme: {}}
}
},
methods: {
getDetail() {
const {id} = this.$route.query
this.instance.post("/app/appdvcpconfig/detail", null, {params: {id}}).then(res => {
if (res?.data) {
let {colorScheme, enableGreyFilter} = res.data
colorScheme = JSON.parse(colorScheme) || {web: 'blue'}
this.form = {colorScheme, enableGreyFilter}
}
})
},
cancel() {
return this.$router.push({})
},
submit() {
this.$refs.ThemeForm.validate(v => {
if (v) {
let {colorScheme} = this.form
colorScheme = JSON.stringify(colorScheme)
this.instance.post("/app/appdvcpconfig/updateSysColorScheme", {...this.form, colorScheme}).then(res => {
if (res?.code == 0) {
this.$message.success("保存成功!")
this.cancel().then(() => location.reload())
}
})
}
})
}
},
created() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.themeSetting {
height: 100%;
.mar-r8 {
margin-right: 8px;
}
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<section class="AiDrag">
<vue-draggable-resizable v-bind="$attrs">
<slot/>
</vue-draggable-resizable>
</section>
</template>
<script>
import 'vue-draggable-resizable/dist/VueDraggableResizable.css'
import VueDraggableResizable from 'vue-draggable-resizable'
export default {
name: "AiDrag",
components: {VueDraggableResizable},
props: {
type: {default: "show"} //show:只拖拽
}
}
</script>
<style lang="scss" scoped>
.AiDrag {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
::v-deep.vdr {
pointer-events: auto;
}
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<section class="AiEditBtn">
<el-button v-if="!edit" type="text" @click="handleOper('edit')">编辑</el-button>
<template v-else>
<el-button type="text" @click="handleOper('submit')">保存</el-button>
<el-button type="text" @click="handleOper('cancel')">取消</el-button>
</template>
</section>
</template>
<script>
export default {
name: "AiEditBtn",
data() {
return {
edit: false
}
},
methods: {
handleOper(event) {
if (event != "submit") {
this.edit = !this.edit
this.$emit(event)
} else this.$emit(event, () => this.edit = !this.edit)
}
}
}
</script>
<style lang="scss" scoped>
.AiEditBtn {
}
</style>

View File

@@ -7,6 +7,6 @@
"dist"
],
"publishConfig": {
"registry": "http://192.168.1.87:4873/"
"registry": "http://cli.sinoecare.net"
}
}

View File

@@ -11,7 +11,7 @@
<main-content class="fill"/>
</el-row>
<div v-if="dialog" class="sign-box">
<ai-sign style="margin: auto" :instance="$axios" :action="{login}"
<ai-sign style="margin: auto" :instance="$request" :action="{login}"
visible @login="getToken" :showScanLogin="false"/>
</div>
<el-button type="info" v-if="!showTools" class="fixedBtn" @click="showTools=true">显示工具栏</el-button>
@@ -22,7 +22,7 @@
import SliderNav from "./components/sliderNav";
import MainContent from "./components/mainContent";
import HeaderNav from "./components/headerNav";
import {mapMutations, mapState} from "vuex";
import {mapActions, mapMutations, mapState} from "vuex";
export default {
name: 'app',
@@ -42,7 +42,8 @@ export default {
}
},
methods: {
...mapMutations(['setToken']),
...mapMutations(['setToken', 'setFinanceUser']),
...mapActions(['getUserInfo']),
getToken(params) {
if (params.access_token) {
this.setToken([params.token_type, params.access_token].join(' '))
@@ -52,25 +53,19 @@ export default {
} else this.$message.error(params.msg || "登录失败!")
},
getUserInfo() {
this.$axios.post("/admin/user/detail-phone").then(res => {
if (res?.data) {
this.$store.commit("setUserInfo", res.data)
if (/^\/project\/xiushan/.test(location.pathname)) {
this.$store.commit("setFinanceUser")
}
}
})
},
handleLogin() {
this.$axios.delete("/auth/token/logout").finally(() => {
this.$request.delete("/auth/token/logout").finally(() => {
this.dialog = true
})
},
},
created() {
if (this.user.token) this.getUserInfo()
wx = jWeixin
if (this.user.token) this.getUserInfo().then(() => {
if (/^\/project\/xiushan/.test(location.pathname)) {
this.setFinanceUser()
}
})
}
}
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -1,5 +1,5 @@
<template>
<div class="headerNav">
<div class="headerNav navBg">
<div style="position: relative">
<ai-icon type="logo" :icon="'iconcunwei'"/>
<ai-icon type="logo" :icon="'iconcunwei'" class="textShadow"/>
@@ -78,7 +78,6 @@ export default {
display: flex;
align-items: center;
width: 100%;
background-image: url("../assets/nav_bg.png");
background-repeat: no-repeat;
background-size: 100% 48px;
position: fixed;

View File

@@ -16,11 +16,19 @@ Vue.use(vcUI);
Vue.use(dvui)
//富文本编辑器配置
Vue.config.productionTip = false;
Vue.prototype.$axios = axios;
Vue.prototype.formatContent = (val) => val.replace(/(\r\n)|(\n)/g, '<br>');
Object.keys(utils).map((e) => (Vue.prototype[e] = utils[e]));
new Vue({
Vue.prototype.$request = axios
const app = new Vue({
router,
store,
render: (h) => h(App)
}).$mount('#app');
render: h => h(App)
});
let theme = null
store.dispatch('getSystem').then(({colorScheme}) => {
theme = JSON.parse(colorScheme || null)
Vue.prototype.$theme = theme?.web || "blue"
return import(`dvcp-ui/lib/styles/theme.${theme?.web}.scss`).catch(() => 0)
}).finally(() => {
!theme ? app.$mount('#app') : import(`dvcp-ui/lib/styles/common.scss`).finally(() => app.$mount('#app'))
})

View File

@@ -14,15 +14,15 @@ export default {
return this.loadApps()
},
loadApps() {
//锁屏loading
waiting.init({innerHTML: '应用加载中..'})
let apps = require.context('../../', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, "lazy")
Promise.all(apps.keys().map(path => apps(path).then(file => {
//新App的自动化格式
let apps = require.context('../../packages/', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, 'lazy'),
projects = require.context('../../project/', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, 'lazy')
const promise = (mods, base) => Promise.all(mods.keys().map(path => mods(path).then(file => {
if (file.default) {
let {name, label} = file.default,
addApp = {
name: path.replace(/\.\/?(vue)?/g, '')?.split("/").join("_"), label: label || name,
path: path.replace(/\.(\/.+\/App.+)\.vue$/, '$1'),
path: `/${base}${path.replace(/\.(\/.+\/App.+)\.vue$/, '$1')}`,
component: appEntry,
module: file.default
}
@@ -31,7 +31,12 @@ export default {
//命名规范入口文件必须以App开头
return store.commit("addApp", addApp)
} else return 0
}))).then(() => {
})))
waiting.init({innerHTML: '应用加载中..'})
Promise.all([
promise(apps, "packages"),
promise(projects, "project")
]).then(() => {
axios.post("/node/wechatapps/addOrUpdate", {
type: "web",
list: this.routes().map(({path: libPath, label, module: {name}, name: id}) => ({

View File

@@ -3,8 +3,7 @@ import {Message} from 'element-ui'
let baseURLs = {
production: "/",
development: '/lan',
oms: '/oms'
development: '/lan'
}
instance.defaults.baseURL = baseURLs[process.env.NODE_ENV]
instance.interceptors.request.use(config => {
@@ -16,15 +15,13 @@ instance.interceptors.request.use(config => {
config.baseURL = "/saas"
} else if (/\/xiushan/.test(location.pathname)) {
config.baseURL = "/xsjr"
config.url = config.url.replace(/(app|auth|admin)\//, "")
} else if (/project\/oms/.test(location.pathname)) {
config.baseURL = "/omsapi"
config.url = config.url.replace(/(app|auth|admin)\//, "")
} else if (/#url-/.test(location.hash)) {
config.baseURL = location.hash.replace(/#url-/, '/')
if (["/xsjr", "/omsapi"].includes(config.baseURL)) {
config.url = config.url.replace(/(app|auth|admin)\//, "")
}
}
if (["/xsjr", "/omsapi"].includes(config.baseURL)) {
config.url = config.url.replace(/(app|auth|admin)\//, "")
}
return config
}, error => Message.error(error))

View File

@@ -1,57 +1,28 @@
import Vue from 'vue'
import Vuex from 'vuex'
import preState from 'vuex-persistedstate'
import request from '../router/axios'
import * as modules from "dvcp-ui/lib/js/modules"
import axios from "../router/axios";
Vue.use(Vuex)
const user = {
state: {
info: {},
token: '',
financeUser: {}
},
mutations: {
setFinanceUser(state) {
request.post("appfinancialorganizationuser/checkUser").then(res => {
state.financeUser = res.data
})
},
setUserInfo(state, userInfo) {
state.info = userInfo
},
setToken(state, token) {
state.token = token
}
}
}
export default new Vuex.Store({
state: {
dicts: [],
apps: []
},
mutations: {
setDicts(state, payload) {
if (payload) {
payload.map(p => {
if (state.dicts.some(d => d.key == p.key)) {
const index = state.dicts.findIndex(d => d.key == p.key)
state.dicts.splice(index, 1)
state.dicts.push(p)
} else {
state.dicts.push(p)
}
})
}
},
addApp(state, app) {
state.apps.push(app)
},
cleanApps(state) {
state.apps = []
},
setFinanceUser(state) {
axios.post("appfinancialorganizationuser/checkUser").then(res => {
state.user.financeUser = res.data
}).catch(() => 0)
}
},
modules: {user},
modules,
plugins: [preState()]
})

View File

@@ -1,6 +1,6 @@
<template>
<section class="appEntry">
<component v-if="app" :is="app" :instance="$axios" :dict="$dict" :permissions="$permissions"/>
<component v-if="app" :is="app" :instance="$request" :dict="$dict" :permissions="$permissions"/>
<ai-empty v-else>无法找到应用文件</ai-empty>
</section>
</template>

View File

@@ -33,7 +33,7 @@
<el-table-column slot="options" width="140px" fixed="right" label="操作" align="center">
<template slot-scope="{ row }">
<div class="table-options">
<el-button type="text" @click="edit('编辑设备配置', row)">编辑</el-button>
<el-button type="text" @click="add('编辑设备配置', row)">编辑</el-button>
<el-button type="text" @click="refresh(row)">刷新</el-button>
<el-button type="text" @click="remove(row.id)">删除</el-button>
</div>
@@ -61,7 +61,7 @@
<el-input v-model.trim="dialogForm.appId" placeholder="请输入..." clearable :maxLength="50" />
</el-form-item>
<el-form-item label="RSA">
<el-input v-model.trim="dialogForm.rsa" placeholder="请输入..." clearable :maxLength="500" type="textarea" :rows="5"/>
<el-input v-model.trim="dialogForm.rsa" placeholder="请输入..." clearable :maxLength="5000" type="textarea" :rows="5"/>
</el-form-item>
<el-form-item label="SECRET">
<el-input v-model.trim="dialogForm.secret" placeholder="请输入..." clearable :maxLength="50" />
@@ -153,7 +153,7 @@
},
methods: {
getListInit() {
this.search.current = 1
this.search.current = 1
this.getList()
},
getList () {
@@ -234,4 +234,4 @@
},
}
}
</script>
</script>

View File

@@ -33,7 +33,7 @@
<el-table-column slot="options" width="140px" fixed="right" label="操作" align="center">
<template slot-scope="{ row }">
<div class="table-options">
<el-button type="text" @click="edit('编辑设备配置', row)">编辑</el-button>
<el-button type="text" @click="add('编辑设备配置', row)">编辑</el-button>
<el-button type="text" @click="refresh(row)">刷新</el-button>
<el-button type="text" @click="remove(row.id)">删除</el-button>
</div>
@@ -141,7 +141,7 @@
},
methods: {
getListInit() {
this.search.current = 1
this.search.current = 1
this.getList()
},
getList () {

View File

@@ -245,7 +245,7 @@
li.active + li {
border-left: 1px solid #D0D4DC;
}
}
}
.newPagination {
width: 100%;

View File

@@ -0,0 +1,56 @@
<template>
<keep-alive include="gmScore">
<component :is="currentPage" v-bind="$props" @change="onChange"/>
</keep-alive>
</template>
<script>
import gridScoreManage from "./components/gridScoreManage.vue"
import gridScoreRules from "./components/gridScoreRules.vue"
import gridScoreStatistics from './components/gridScoreStatistics.vue'
import gridScoreDetail from './components/gridScoreDetail.vue'
import gmScore from './components/gmScore.vue'
export default {
name: 'AppGridMemberScore',
label: "网格员积分",
props: {
instance: Function,
dict: Object,
permissions: Function,
},
computed: {
currentPage() {
let {hash} = this.$route
return hash == "#gridScoreDetail" ? gridScoreDetail :
hash == "#gridScoreRules" ? gridScoreRules :
hash == "#gridScoreStatistics" ? gridScoreStatistics :
hash == "#gridScoreManage" ? gridScoreManage : gmScore
}
},
components: {
gmScore,
gridScoreManage,
gridScoreRules,
gridScoreStatistics,
gridScoreDetail,
},
methods: {
onChange(data) {
let {type, params: query} = data,
hash = ["gridScoreManage", "gridScoreRules","gridScoreStatistics"].includes(type) ? "" : "#" + type
this.$router.push({hash, query})
}
}
}
</script>
<style lang="scss" scoped>
.AppGridMemberScore {
height: 100%;
width: 100%;
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<ai-list class="AppGridMemberScore">
<template slot="title">
<ai-title title="网格员积分" :isShowBottomBorder="false" :instance="instance" >
<template slot="sub">
<div>网格员可通过完成某些任务获取一定数量的积分积分可去兑换相应的奖励</div>
</template>
</ai-title>
</template>
<template slot="tabs">
<el-tabs v-model="currIndex">
<el-tab-pane v-for="(tab,i) in tabs" :key="i" :label="tab.label">
<component :is="tab.comp" v-if="currIndex === String(i)" :ref="tab.name" v-on="$listeners"
:areaId="areaId" :instance="instance" :dict="dict" :permissions="permissions"/>
</el-tab-pane>
</el-tabs>
</template>
</ai-list>
</template>
<script>
import girdScoreManage from "./gridScoreManage.vue"
import gridScoreRules from "./gridScoreRules.vue"
import gridScoreStatistics from './gridScoreStatistics.vue'
import {mapState} from 'vuex'
export default {
name: 'AppGridMemberScore',
label: "网格员积分",
components: {
girdScoreManage,
gridScoreRules,
gridScoreStatistics
},
props: {
instance: Function,
dict: Object,
permissions: Function,
},
data() {
return {
currIndex: "0",
areaId: '',
}
},
computed: {
...mapState(['user']),
tabs() {
return [
{
label: "积分管理",
name: "girdScoreManage",
comp: girdScoreManage,
permission: "",
},
{
label: "积分规则",
name: "gridScoreRules",
comp: gridScoreRules,
permission: "",
},
{
label: "积分统计",
name: "gridScoreStatistics",
comp: gridScoreStatistics,
permission: "",
},
]
}
},
created() {
this.areaId = this.user.info.areaId
// this.$dict.load("")
},
methods: {
},
}
</script>
<style lang="scss" scoped>
.AppGridMemberScore {
height: 100%;
width: 100%;
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<section class="gridScoreDetail">
<ai-title slot="title" title="网格员积分详情" isShowBottomBorder :isShowBack="true" @onBackClick="cancel(false)"/>
<el-row style="margin-top: 20px;">
<div class="card_list">
<div class="card">
<h2>姓名</h2>
<p class="color1">{{ data.userName }}</p>
</div>
<div class="card">
<h2>积分余额</h2>
<p class="color2">{{ data.integral || 0 }}</p>
</div>
<div class="card">
<h2>已用积分</h2>
<p class="color3">{{ data.usedIntegral || 0 }}</p>
</div>
</div>
</el-row>
<el-row class="echertsBox" style="margin-bottom: 16px">
<div class="title">
<h4>事件汇总</h4>
<div class="timecSelect">
时间<el-date-picker size="small" value-format="yyyy-MM-dd" @change="timeChange" v-model="timeList" type="daterange" range-separator="至" :start-placeholder="startPla" :end-placeholder="endPla"></el-date-picker>
</div>
</div>
<div class="bar_Box">
<div id="chartDom" style="height: 230px; width: 100%;" v-show="xData.length && yData.length"></div>
<ai-empty style="height: 200px; width: 100%;" v-show="!xData.length && !yData.length"></ai-empty>
</div>
</el-row>
<ai-card>
<ai-title slot="title" title="余额变动明细"/>
<template #content>
<ai-search-bar>
<template #left>
<ai-select v-model="search.type" placeholder="请选择类型" @change="search.current=1,getIntegralChange()"
:selectList="dict.getDict('integralType')"/>
</template>
<template #right>
<ai-download :instance="instance" :url="`/app/appintegraluser/changeIntegralExport?id=${$route.query.id}`" :params="search" fileName="网格员余额变动明细"
:disabled="tableData.length == 0">
<el-button size="small">导出</el-button>
</ai-download>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="total" :current.sync="search.current" :size.sync="search.size"
@getList="getIntegralChange" :col-configs="colConfigs" :dict="dict">
<el-table-column slot="changeIntegral" label="变动积分" align="center">
<template slot-scope="{ row }">
<span v-if="row.integralType == 3">{{ row.changeIntegral | formatTime }}</span>
<span v-if="row.integralType == 0">{{ row.integralCalcType == 0 ? '-' : '+' }}{{ row.changeIntegral }}</span>
</template>
</el-table-column>
<el-table-column slot="integralType" label="类型" align="center">
<template slot-scope="{ row }">
<span v-if="row.integralType == 0">积分调整</span>
<span v-else>{{ row.eventType }}</span>
</template>
</el-table-column>
<el-table-column slot="eventDesc" label='事件' align="center" width="400px" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.integralType == 0">{{ row.eventDesc }}</span>
<span v-else>{{ row.eventName }}</span>
</template>
</el-table-column>
</ai-table>
</template>
</ai-card>
</section>
</template>
<script>
import dayjs from "dayjs";
import * as echarts from 'echarts';
export default {
name: "gridScoreDetail",
data() {
return {
myChart: null,
tableData: [],
search: {
name: '',
girdId: '',
type: '',
current: 1,
size: 10,
},
total: 0,
girdList: [],
timeList: [],
data: {},
startTime: '',
endTime: '',
xData: [],
yData: [],
startPla: '',
endPla: ''
}
},
props: {
instance: Function,
dict: Object,
permissions: Function,
},
computed: {
colConfigs() {
return [
{ prop: "doTime", label: '时间', align: "left", width: "200px" },
{ slot: "integralType", label: '类型', align: "center", width: "240px", dict:"integralType"},
{ slot: "changeIntegral"},
{ prop: "nowIntegral", label: '剩余积分', align: "center",width: "200px" },
{ slot: "eventDesc"},
]
}
},
created() {
this.$dict.load('integralType').then(() => {
this.getDetail()
this.getIntegralChange()
this.getEventSummary()
let nowTime = dayjs().format('YYYY-MM-DD')
let timeAgo = dayjs().subtract(29, 'day').format('YYYY-MM-DD')
this.startPla = timeAgo
this.endPla = nowTime
})
},
methods: {
// 详情
getDetail() {
this.instance.post(`/app/appintegraluser/girdDetail`,null,{
params: {
id: this.$route.query.id
}
}).then(res=>{
if(res?.data) {
this.data = res.data
}
})
},
// 事件汇总
getEventSummary() {
this.instance.post(`/app/appintegraluser/eventSummary`,null,{
params: {
id: this.$route.query.id,
startTime: this.startTime,
endTime: this.endTime,
}
}).then(res=>{
if(res?.data) {
this.xData = res.data.map(x=> x.eventName)
this.yData = res.data.map(y=> y.totalIntegral)
this.getColEcherts(this.xData, this.yData)
}
})
},
// 余额变动明细
getIntegralChange() {
this.instance.post(`/app/appintegraluser/getChangeDetail`, null, {
params: {
...this.search, //积分类型
total: this.total,
id: this.$route.query.id,
}
}).then(res => {
if(res?.data) {
this.tableData = res.data.records
this.total = res.data.total
}
})
},
timeChange() {
if(this.timeList.length) {
this.startTime = this.timeList[0]
this.endTime = this.timeList[1]
this.getEventSummary()
}
},
getColEcherts(xData, yData) {
let chartDom = document.getElementById('chartDom');
chartDom.style.width = window.innerWidth - 335 + "px";
this.myChart = echarts.init(chartDom);
this.myChart.setOption({
dataZoom: [
{
type: "slider",
xAxisIndex: [0],
filterMode: "filter",
},
],
grid: {
left: '16px',
right: '28px',
bottom: '14px',
top: '30px',
containLabel: true
},
xAxis: {
type: 'category',
data: xData,
},
yAxis: {
type: 'value',
},
series: [
{
data: yData,
type: 'bar',
itemStyle: {
normal: {
color: "#5087ec",
label: {
show: true, //开启显示
position: 'top', //在上方显示
textStyle: {
fontSize: 13,
color: '#666'
}
},
},
},
barWidth: 20,
barGap: '20%',
}
]
}, true);
window.addEventListener("resize", this.onResize)
},
onResize() {
this.myChart.resize()
},
cancel(isRefresh) {
this.$emit('change', {
type: 'gridScoreManage',
isRefresh: !!isRefresh
})
}
},
filters: {
formatTime(num) {
if(num > 0) {
return '+' + num
} else {
return num
}
}
},
mounted() {
this.getColEcherts()
},
destroyed () {
window.removeEventListener('resize', this.onResize)
},
}
</script>
<style lang="scss" scoped>
.gridScoreDetail {
width: 100%;
height: 100%;
padding: 0 20px;
box-sizing: border-box;
overflow-y: scroll;
.card_list {
display: flex;
.card {
flex: 1;
height: 96px;
background: #FFFFFF;
box-shadow: 0px 4px 6px -2px rgba(15,15,21,0.1500);
border-radius: 4px;
margin-right: 20px;
padding: 16px 24px;
box-sizing: border-box;
h2 {
color: #888888;
font-weight: 600;
font-size: 16px;
}
p {
margin-top: 8px;
font-size: 24px;
font-weight: 600;
}
.color1 {
color: #2891FF;
}
.color2 {
color: #22AA99;
}
.color3 {
color: #F8B425;
}
}
.card:last-child {
margin-right: 0;
}
}
.echertsBox {
width: 100%;
margin-top: 20px;
background: #FFFFFF;
box-shadow: 0px 4px 6px -2px rgba(15,15,21,0.1500);
border-radius: 4px;
padding: 16px;
box-sizing: border-box;
.title {
display: flex;
justify-content: space-between;
h4 {
color: #222222;
font-style: 16px;
font-weight: 600;
}
}
.bar_Box {
width: 100%;
#chartDom {
width: 100%;
height: 230px;
margin-top: 16px;
}
}
}
}
</style>

View File

@@ -0,0 +1,361 @@
<template>
<section class="gridScoreManage">
<ai-list>
<template #content>
<ai-search-bar>
<template #left>
<el-button type="primary" size="small" icon="iconfont iconAdd" @click="changeIntegral('',0)">&nbsp;批量调整积分</el-button>
<el-cascader ref="cascader1" clearable v-model="girdIdList" :options="girdOptions" placeholder="所属网格" size="small"
:props="defaultProps" :show-all-levels="false" @change="gridChange"></el-cascader>
</template>
<template #right>
<el-input size="small" placeholder="姓名" v-model="search.userName" clearable
@clear="current = 1, search.userName = '', getTableData()" suffix-icon="iconfont iconSearch"
v-throttle="() => {(current = 1), getTableData();}"/>
<ai-download :instance="instance" url="/app/appintegraluser/girdIntegralExport" :params="search" fileName="网格员积分"
:disabled="tableData.length == 0">
<el-button size="small">导出</el-button>
</ai-download>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="total" :current.sync="current" :size.sync="size"
@getList="getTableData()" :col-configs="colConfigs" :dict="dict" @sort-change="changeTableSort">
<el-table-column slot="options" label="操作" align="center">
<template slot-scope="{ row }">
<el-button type="text" @click="changeIntegral(row,1)">调整积分</el-button>
<el-button type="text" @click="toDetail(row.id)">详情</el-button>
</template>
</el-table-column>
</ai-table>
</template>
</ai-list>
<ai-dialog
title="调整积分"
:visible.sync="dialog"
:destroyOnClose="true"
width="720px"
@onConfirm="onConfirm"
@closed="form={},chooseUserList=[]">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="选择人员" prop="ids">
<ai-person-select :instance="instance" :customClicker="true" :chooseUserList="chooseUserList"
url="/app/appgirdmemberinfo/list" headerTitle="网格员列表"
:isMultiple="true" dialogTitle="选择" @selectPerson="selectPerson" class="aipersonselect">
<template name="option" v-slot:option="{ item }">
<span class="iconfont iconProlife">{{ item.name }}</span>
<ai-id mode="show" :show-eyes="false" :value="item.idNumber"/>
</template>
</ai-person-select>
</el-form-item>
<el-form-item label="调整说明" prop="eventDesc">
<el-input v-model.trim="form.eventDesc" placeholder="请输入..." type="textarea" :rows="4" show-word-limit
maxlength="100"></el-input>
</el-form-item>
<el-form-item label="上传凭证">
<ai-uploader :instance="instance" fileType="file" v-model="form.file" :limit="1"></ai-uploader>
</el-form-item>
<el-form-item label="类型" prop="integralCalcType">
<ai-select v-model="form.integralCalcType" :selectList="dict.getDict('integralCalcType')"/>
</el-form-item>
<el-form-item label="积分" prop="integral">
<el-input v-model.trim="form.integral" placeholder="请输入正数" size="small"></el-input>
</el-form-item>
</el-form>
</ai-dialog>
</section>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "gridScoreManage",
label: "积分管理",
props: {
instance: Function,
dict: Object,
permissions: Function,
},
data() {
return {
search: {
userName: '',
girdId: '',
current: 1,
size: 10,
sortFiled: '',
sortRule: '',
},
girdIdList: [],
tableData: [],
size: 10,
total: 0,
current: 1,
girdList: [],
form: {
ids: [],
eventDesc: "",
enclosure: "", // 附件
integralCalcType: "",
integral: '',
file: [],
},
personList: [],
dialog: false,
girdOptions: [],
defaultProps: {
label: 'girdName',
value: 'id',
checkStrictly: true,
},
chooseUserList: [],
flag: false,
}
},
created() {
this.$dict.load('integralCalcType')
this.getTableData()
this.getGridList()
},
computed: {
...mapState(['user']),
colConfigs() {
return [
{ prop: "userName", label: '姓名', align: "left", },
{ prop: "girdName", label: '所属网格' },
{ prop: "integral", label: '积分余额', align: "center", sortable: "custom" },
{ prop: "totalIntegral", label: '累计积分', align: "center", sortable: "custom" },
{ prop: "usedIntegral", label: '已用积分', align: "center", sortable: "custom" },
{ slot: "options" },
]
},
rules() {
return {
ids: [{required: true, message: '请选择人员', trigger: 'blur'}],
eventDesc: [{required: true, message: '请输入调整说明', trigger: 'blur'}],
integralCalcType: [{required: true, message: '请选择类型', trigger: 'change'}],
integral: [{required: true, message: '请输入积分', trigger: 'blur' },
{pattern: /^([1-9]\d*|0)(\.\d{1,2})?$/, message: '请输入正数且最多只能保留两位小数'}],
}
},
},
methods: {
getTableData() {
this.instance.post(`/app/appintegraluser/integralManager`,null,{
params: {
...this.search,
current: this.current,
size: this.size,
total: this.total
}
}).then(res => {
if(res?.data) {
this.tableData = res.data.records
this.total = res.data.total
}
})
},
selectPerson(val) {
if (val) {
this.personList = val
this.form.ids = [...this.personList.map(e => e.id)]
} else {
this.form.ids = this.chooseUserList.map(e => e.id)
}
},
changeIntegral(row,type) {
if(type==0) {
this.dialog = true
} else if(type ==1) {
this.chooseUserList = [{
id: row.userId,
name: row.userName
}]
this.form.ids = this.chooseUserList.map(e => e.id)
this.dialog = true
}
},
getGridList() {
this.instance.post(`/app/appgirdinfo/listAll3`).then((res) => {
if (res.code == 0) {
this.girdOptions = this.toTree(res.data)
}
})
},
// 转树形结构
toTree(data) {
let result = [];
if (!Array.isArray(data)) {
return result
}
let map = {};
data.forEach(item => {
map[item.id] = item;
});
data.forEach(item => {
let parent = map[item.parentGirdId];
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
},
gridChange(val) {
this.girdIdList = val
this.search.girdId = val?.[val.length - 1]
this.$refs.cascader1.dropDownVisible = false;
this.getTableData()
},
changeTableSort(col) {
if(col.prop === 'integral') { // 剩余积分
this.search.sortFiled = 0
if(col.order === 'ascending') {
this.search.sortRule = true
} else if(col.order === 'descending') {
this.search.sortRule = false
} else if(col.order === null) {
this.search.sortRule = ''
}
} else if(col.prop === 'totalIntegral') { // 累计积分
this.search.sortFiled = 1
if(col.order === 'ascending') {
this.search.sortRule = true
} else if(col.order === 'descending') {
this.search.sortRule = false
} else if(col.order === null) {
this.search.sortRule = ''
}
} else if(col.prop === 'usedIntegral') { // 已用积分
this.search.sortFiled = 2
if(col.order === 'ascending') {
this.search.sortRule = true
} else if(col.order === 'descending') {
this.search.sortRule = false
} else if(col.order === null) {
this.search.sortRule = ''
}
}
this.getTableData()
},
onConfirm() {
if(this.flag) return
if(this.form.file?.length) {
this.form.enclosure = this.form.file[0].url
}
this.$refs.form.validate((valid)=> {
if(valid) {
this.flag = true
this.instance.post(`/app/appintegraluser/changeIntegral`,{
ids: this.form.ids,
eventDesc: this.form.eventDesc,
enclosure: this.form.enclosure, // 附件
integralCalcType: this.form.integralCalcType,
integral: this.form.integral,
}).then(res => {
if(res?.code == 0) {
this.$message.success('调整积分成功')
setTimeout(() =>{
this.dialog = false
this.getTableData()
this.flag = false
}, 600)
} else {
this.flag = false
}
})
}
})
},
toDetail(id) {
this.$emit('change', {
type: 'gridScoreDetail',
params: {
id: id
}
})
}
},
}
</script>
<style lang="scss" scoped>
.gridScoreManage {
height: 100%;
::v-deep .ai-dialog .ai-dialog__content {
max-height: 600px!important;
}
.userlist {
display: inline-block;
}
.userlist, .user {
display: inline-block;
}
.user {
position: relative;
width: 70px;
text-align: center;
.remove-icon {
position: absolute;
right: 7px;
top: -4px;
line-height: 1;
padding: 6px 0;
font-size: 16px;
cursor: pointer;
&:hover {
color: crimson;
}
}
img, h2 {
display: block;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
margin: 0 auto 4px;
font-size: 14px;
color: #fff;
border-radius: 50%;
}
h2 {
background-color: $primaryColor;
}
span {
color: #666;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
}
}
::v-deep .selectCont .pagination {
width: 100%!important;
background: pink;
}
}
</style>

View File

@@ -0,0 +1,482 @@
<template>
<section class="gridScoreRules">
<!-- v-if="permissions('app_appvillagerintegralrule_detail')" -->
<ai-list>
<template slot="content">
<ai-search-bar>
<template #left>
<el-button type="primary" icon="iconfont iconAdd" @click="dialog = true">&nbsp;添加</el-button>
<el-cascader size="small" v-model="systemRuleIdList" :options="rulesOps" placeholder="请选择事件/类型" clearable :props="rulesProps"
@change="handleTypeSearch" ref="eventTypeSearch"/>
<ai-select v-model="search.status" @change="(page.current = 1), getList()" placeholder="请选择状态" :selectList="$dict.getDict('integralRuleStatus')">
</ai-select>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :col-configs="colConfigs" :total="page.total" :dict="dict" :current.sync="page.current" :size.sync="page.size"
@getList="getList()">
<el-table-column slot="integral" label="分值" align="center">
<template slot-scope="{ row }">
<!-- <span v-if="row.integralValueType == 1">
{{ row.integralStart > 0 ? "+" + row.integralStart : row.integralStart }}~{{ row.integralEnd > 0 ? "+" + row.integralEnd : row.integralEnd }}
</span> -->
<span>{{ row.integral > 0 ? "+" : "" }}{{ row.integral }}</span>
</template>
</el-table-column>
<el-table-column slot="options" label="操作" align="center" fixed="right" width="200">
<template slot-scope="{ row }">
<div class="table-options">
<el-button type="text" @click="changeStatus(row.id, 0)" v-if="row.status == 1">停用</el-button>
<el-button type="text" @click="changeStatus(row.id, 1)" v-else>启用</el-button>
<el-button type="text" @click="toEdit(row)">编辑</el-button>
<el-button type="text" @click="remove(row.id)">删除</el-button>
</div>
</template>
</el-table-column>
</ai-table>
</template>
</ai-list>
<!-- <ai-empty v-else>暂无应用权限</ai-empty> -->
<ai-dialog :title="dialogTitle" :visible.sync="dialog" @onConfirm="onConfirm" @closed="closed" width="900px" @open="beforeSelectTree">
<div class="form_div">
<el-form ref="DialogForm" :model="form" :rules="formRules" size="small" label-suffix="" label-width="150px">
<el-form-item label="事件类型" prop="systemRuleId">
<el-cascader v-model="form.systemRuleId" ref="cascaderArr" :props="etOps" clearable placeholder="请选择" @change="handleTypeForm" :options="rulesOps"/>
</el-form-item>
<el-form-item label="自定义事件" v-if="form.systemRuleId == '自定义'" prop="ruleName" :required="form.systemRuleId == '自定义'">
<el-input placeholder="请输入,周期范围内,不填写表示不限制" v-model="form.ruleName" clearable maxlength="10" show-word-limit/>
</el-form-item>
<el-form-item label="规则">
<div>常规</div>
</el-form-item>
<!-- <el-form-item label="规则" prop="ruleType" v-if="form.ruleType>-1" required>
<el-row type="flex" justify="space-between">
<div v-text="$dict.getLabel('integralRuleRuleType',form.ruleType)"/>
<el-button v-if="form.ruleType==1" type="text" icon="iconfont iconAdd"
@click="form.ladderRule.push({viewCount:null,integral:null})">添加
</el-button>
</el-row>
<el-table v-if="form.ruleType==1" :data="form.ladderRule" size="mini" border stripe>
<el-table-column label="查看人数(人)" align="center">
<template slot-scope="{row}">
<el-input class="tableInput" v-model.number="row.viewCount" clearable placeholder="请输入"/>
</template>
</el-table-column>
<el-table-column label="获得积分(分)" align="center">
<template slot-scope="{row}">
<el-input class="tableInput" v-model="row.integral" clearable placeholder="请输入" type="number"
@keyup.native="row.integral=checkIntegral(row.integral)"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="{$index}">
<el-button type="text" @click="handleDelete($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item> -->
<el-form-item label="周期范围" prop="scoringCycle">
<ai-select v-model="form.scoringCycle" :selectList="$dict.getDict('integralRuleScoringCycle')"/>
</el-form-item>
<el-form-item label="奖励次数">
<el-input type="number" placeholder="请输入,周期范围内,不填写表示不限制" v-model.number="form.numberLimit" clearable/>
</el-form-item>
<el-form-item label="积分分值" prop="integral">
<el-input placeholder="请输入" v-model="form.integral" clearable/>
</el-form-item>
<el-form-item label="有效范围" prop="validRangeType" required>
<el-radio-group v-model="form.validRangeType">
<el-radio label="0">全局</el-radio>
<el-radio label="1">指定网格</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="生效网格" :prop="form.validRangeType == 1 ? 'validRangeData' : ''"
:rules="[{ required: true, message: '请选择生效网格', trigger: 'change' }, ]" v-if="form.validRangeType == 1">
<ai-dialog-btn dialogTitle="选择网格" append-to-body @onConfirm="getCheckedTree" :customFooter="false" :text="girdInfoList.length ? '重新选择' : '请选择'">
<div class="grid">
<el-tree :data="treeObj.treeList" :props="treeObj.defaultProps" node-key="id" :expand-on-click-node="false">
<template slot-scope="{data}">
<el-row class="fill" type="flex" @click.native.stop="handleTreeChecked(data)">
<div class="fill" v-text="data.girdName"/>
<div class="iconfont iconSuccess color-primary mar-r8" v-if="data.checked"/>
</el-row>
</template>
</el-tree>
</div>
</ai-dialog-btn>
<div v-if="girdInfoList.length">
<span v-for="(e,index) in girdNameList" :key="index" class="mar-r8" v-text="e"/>
</div>
</el-form-item>
</el-form>
</div>
</ai-dialog>
</section>
</template>
<script>
export default {
name: "gridScoreRules",
label: "积分规则",
props: {
instance: Function,
dict: Object,
permissions: Function,
},
data() {
var validcode = (rule, value, callback) => {
if (value) {
if (value != 0) {
if (!/^([+-]?([1-9]{1}\d*)|(0{1}))(\.\d{1,2})?$/.test(value)) {
callback(new Error('请输入积分分值,可输入正数、负数、最多保留两位小数'))
} else {
callback();
}
} else {
callback(new Error('请输入有效的积分分值'));
}
} else {
callback(new Error('请输入积分分值'));
}
}
return {
search: {
status: "",
systemRuleId: "",
ruleName: ""
},
systemRuleIdList: [],
page: {current: 1, size: 10, total: 0},
colConfigs: [
{
prop: "parentRuleName",
label: "类型",
dict: "integralRuleEventType",
},
{prop: "ruleName", label: "事件", dict: "integralRuleEvent"},
{prop: "ruleType", label: "规则", dict: "integralRuleRuleType"},
{
prop: "scoringCycle",
label: "周期范围",
dict: "integralRuleScoringCycle",
render: (h, {row}) => {
return h(
"span",
{},
row.numberLimit.length
? $dict.getLabel("integralRuleScoringCycle", row.scoringCycle)
: $dict.getLabel("integralRuleScoringCycle", row.scoringCycle) +
row.numberLimit +
"次"
);
},
},
{slot: "integral", label: "积分分值", align: "center"},
{
prop: "validRangeType",
label: "有效范围",
formart: (v) => (v == 0 ? "全局" : "指定网格"),
},
{
prop: "status",
label: "状态",
align: "center",
width: 96,
dict: "integralRuleStatus",
},
{slot: "options", label: "操作", align: "center"},
],
tableData: [],
dialog: false,
form: {
ruleType: "0",
systemRuleId: "",
ruleName: "",
scoringCycle: "",
numberLimit: "",
integral: "",
validRangeType: "0",
validRangeData: "",
},
formRules: {
systemRuleId: [
{required: true, message: "请选择事件/类型", trigger: "change"},
],
ruleName: [
{required: true, message: "请输入自定义事件", trigger: "change"},
],
scoringCycle: [
{required: true, message: "请选择周期范围", trigger: "change"},
],
integral: [{required: true, validator: validcode, trigger: "blur"},],
validRangeType: [
{required: true, message: "请选择有效范围", trigger: "change"},
],
},
rulesOps: [],
rulesProps: {
label: "ruleName",
value: "id",
checkStrictly: true,
},
radio: 0,
treeObj: {
treeList: [],
defaultProps: {
label: "girdName",
value: "id",
children: 'children',
isLeaf: 'leaf'
},
},
treeSelected: {},
girdInfoList: [],
rulueType: "0",
girdNameList: [],
list: [],
};
},
created() {
this.$dict.load("integralRuleStatus", "integralRuleRuleType", "integralRuleScoringCycle",
"integralRuleEvent", "integralRuleEventType").then(() => {
this.getList();
this.getRulesList();
});
},
methods: {
getList() {
this.instance
.post(`/app/appintegralrule/list`, null, {
params: {
...this.search,
...this.page,
},
})
.then((res) => {
if (res?.data) {
this.tableData = res.data.records;
this.page.total = res.data.total;
}
});
},
closed() {
this.form = {
ruleType: "0",
systemRuleId: "",
ruleName: "",
scoringCycle: "",
numberLimit: "",
integral: "",
validRangeType: "0",
validRangeData: "",
};
this.girdInfoList = []
this.treeSelected = {}
},
toEdit(row) {
this.form = {...row}
if (this.form?.validRangeData) {
this.girdInfoList = JSON.parse(this.form.validRangeData)
this.girdNameList = this.girdInfoList.map(e => e.girdName)
}
this.$nextTick(() => {
this.dialog = true;
});
},
remove(id) {
this.$confirm("删除后不可恢复,是否要删除该规则?", {
type: "error",
}).then(() => {
this.instance
.post(`/app/appintegralrule/delete?ids=${id}`)
.then((res) => {
if (res.code == 0) {
this.$message.success("删除成功!");
this.getList();
}
});
});
},
changeStatus(id, status) {
let text = status == 1 ? "启用" : "停用";
this.$confirm(`确定${text}该条规则?`).then(() => {
this.instance
.post(`/app/appintegralrule/enableStatus?id=${id}`)
.then((res) => {
if (res.code == 0) {
this.$message.success(`${text}成功!`);
this.getList();
}
});
});
},
onConfirm() {
this.$refs.DialogForm.validate((valid) => {
if (valid) {
let formData = this.$copy(this.form);
// formData.ladderRule = JSON.stringify(formData.ladderRule)
formData.integral = formData.integral || 0;
this.instance
.post(`/app/appintegralrule/addOrUpdate`, formData)
.then((res) => {
if (res.code == 0) {
this.$message.success(
`${this.isEdit ? "编辑成功" : "添加成功"}`
);
this.dialog = false;
this.getList();
this.closed();
this.girdInfoList = []
this.girdNameList = []
}
});
} else {
return false;
}
});
},
handleTypeSearch(v) {
this.systemRuleIdList = v
this.search.systemRuleId = v?.[v.length - 1];
this.search.ruleName = this.$refs.eventTypeSearch.getCheckedNodes()[0]?.label
this.page.current = 1;
this.$refs.eventTypeSearch.dropDownVisible = false;
this.getList();
},
handleTypeForm(v) {
if (this.dialog) {
this.form.systemRuleId = v?.[v.length - 1];
}
},
handleDelete(i) {
this.$confirm("是否要删除该规则?")
.then(() => {
this.form.ladderRule.splice(i, 1);
})
.catch(() => 0);
},
checkIntegral(v) {
return /\.\d{2,}$/.test(v) ? Math.abs(v).toFixed(1) : Math.abs(v);
},
getRulesList() {
this.instance
.post(`/app/appintegralsystemrule/list?current=1&sizes=3000`)
.then((res) => {
if (res?.data) {
this.rulesOps = this.toTree(res.data.records);
this.rulesOps.push({
ruleName: "自定义",
id: "自定义",
});
}
});
},
// 转树形结构
toTree(data) {
let result = [];
if (!Array.isArray(data)) {
return result;
}
let map = {};
data.forEach((item) => {
map[item.id] = item;
});
data.forEach((item) => {
let parent = map[item.parentRuleId];
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
},
getCheckedTree() {
const selected = Object.values(this.treeSelected)
if (!selected.length) {
return this.$message.error("请选择网格");
}
this.girdInfoList = selected.map((item) => {
return {...item, checkType: true};
});
let validRangeData = selected.map((e) => ({id: e.id, girdName: e.girdName}))
this.girdNameList = validRangeData.map(e => e.girdName)
this.form.validRangeData = JSON.stringify(validRangeData)
},
beforeSelectTree() {
this.instance.post(`/app/appgirdinfo/listAll3`, null, null).then((res) => {
if (res?.data) {
this.list = res.data.map(e => ({...e, checked: !!this.girdInfoList.find(s => s.id == e.id)}))
this.girdInfoList.map(e => this.treeSelected[e.id] = e)
this.treeObj.treeList = this.$arr2tree(this.list, {parent: 'parentGirdId'})
}
});
},
handleTreeChecked(data) {
this.list.forEach(v => {
return {
...v,
checked: false
}
})
data.checked = !data.checked
if (data.checked) {
this.treeSelected[data.id] = data
} else {
delete this.treeSelected[data.id]
}
}
},
computed: {
isEdit() {
return !!this.form.id;
},
dialogTitle() {
return this.isEdit ? "编辑积分规则" : "添加积分规则";
},
etOps() {
return {
value: "id",
label: "ruleName",
};
},
},
};
</script>
<style lang="scss" scoped>
.gridScoreRules {
height: 100%;
background: #f3f6f9;
::v-deep .ai-list__content--right {
width: 100%;
}
// ::v-deep .searchRightZone {
// display: flex;
// }
::v-deep .ai-dialog {
.el-cascader {
width: 100%;
}
.tableInput {
& > input {
text-align: center;
border: none;
background: transparent;
}
}
}
}
</style>

View File

@@ -0,0 +1,653 @@
<template>
<section class="gridScoreStatistics">
<el-row class="overallStatistics">
<div class="title">
<p>总体统计</p>
<div class="title_right">
<div>
<span v-for="(item,index) in timeCheck" :key="index" :class="type == index? 'active':''"
@click="timeChange(index)">{{ item }}</span>
</div>
<el-cascader ref="cascader1" v-model="girdArr" :options="girdOptions" placeholder="所属网格" size="small"
:props="defaultProps" :show-all-levels="false" @change="gridChange" clearable></el-cascader>
</div>
</div>
<div class="card_list">
<div class="card">
<h2>积分余额汇总
<el-tooltip
placement="right"
style="width: 16px;"
content="截止目前所有网格员剩余可用积分余额的总和">
<i class="el-icon-warning-outline"></i>
</el-tooltip>
</h2>
<p class="color1">{{ data.nowIntegral || 0 }}</p>
</div>
<div class="card">
<h2>发放积分</h2>
<p class="color1">{{ data.addIntegral || 0 }}</p>
</div>
<div class="card">
<h2>消耗积分</h2>
<p class="color1">{{ data.reduceIntegral || 0 }}</p>
</div>
</div>
<div class="echertsBox">
<div class="left_Box">
<p>个人积分排行</p>
<div>
<div id="chart1" style="height: 300px; width: 100%;" v-show="userSortListX.length && userSortListY.length"></div>
<ai-empty v-show="!userSortListX.length && !userSortListY.length" style="height: 200px; width: 100%;" id="empty"></ai-empty>
</div>
</div>
<div class="right_Box">
<p>网格积分排行</p>
<div>
<div id="chart2" style="height: 300px; width: 100%;" v-show="girdSortListX.length && girdSortListY.length"></div>
<ai-empty v-show="!girdSortListX.length && !girdSortListY.length" style="height: 200px; width: 100%;" id="empty"></ai-empty>
</div>
</div>
</div>
</el-row>
<ai-card>
<ai-title slot="title" title="积分明细"/>
<template #content>
<ai-search-bar>
<template #left>
<el-cascader ref="cascader2" v-model="girdIdArr" :options="girdOptions" placeholder="所属网格" size="small"
:props="defaultProps" :show-all-levels="false" clearable @change="gridChangeOpt"></el-cascader>
<ai-select v-model="search.integralType" placeholder="请选择类型" @change="current=1, getTableData()"
:selectList="dict.getDict('integralType')"/>
<el-date-picker v-model="time" size="small" type="daterange" value-format="yyyy-MM-dd"
range-separator="" start-placeholder="开始日期" end-placeholder="结束日期" @change="onChange">
</el-date-picker>
</template>
<template #right>
<el-input size="small" placeholder="请输入姓名" v-model="search.userName" clearable
@clear="current = 1, search.userName = '', getTableData()" suffix-icon="iconfont iconSearch"
v-throttle="() => {(current = 1), getTableData();}" />
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="total" :current.sync="current" :size.sync="size"
@getList="getTableData" :col-configs="colConfigs" :dict="dict">
<el-table-column slot="eventDesc" label='事件' align="center" width="400px" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.integralRuleId">{{ row.integralRuleName }}</span>
<span v-else>{{ row.eventDesc }}</span>
</template>
</el-table-column>
<el-table-column slot="integralType" label="类型" align="center">
<template slot-scope="{ row }">
<span v-if="row.integralRuleId">{{ row.eventType }}</span>
<span v-else>{{ row.integralRuleName }}</span>
</template>
</el-table-column>
<el-table-column slot="changeIntegral" label="积分变动" align="center">
<template slot-scope="{ row }">
<span v-if="row.integralType == 3">{{ row.changeIntegral | formatTime }}</span>
<span v-if="row.integralType == 0">{{ row.integralCalcType == 0 ? '-' : '+' }}{{ row.changeIntegral }}</span>
</template>
</el-table-column>
<el-table-column slot="options" label="操作" align="center">
<template slot-scope="{ row }">
<el-button type="text" @click="open(row.id)">详情</el-button>
</template>
</el-table-column>
</ai-table>
</template>
</ai-card>
<el-dialog title="详情" :visible.sync="dialog" customFooter width="700">
<ai-detail>
<template #content>
<ai-wrapper>
<ai-info-item label="姓名" :value="details.integralUserName" />
<ai-info-item label="所属网格" :value="details.girdName"/>
<ai-info-item label="事件" isLine :value="details.eventDesc">
<span v-if="details.integralRuleId">{{ details.integralRuleName }}</span>
<span v-else>{{ details.eventDesc }}</span>
</ai-info-item>
<ai-info-item label="时间" isLine :value="details.createTime"/>
<ai-info-item label="积分变动" v-if="details.integralType == 3">
{{ details.changeIntegral | formatTime }}
</ai-info-item>
<ai-info-item label="积分变动" v-if="details.integralType == 0">
{{ details.changeIntegral > 0 ? '+' : '-' }}{{ details.changeIntegral }}
</ai-info-item>
<ai-info-item label="积分余额" :value="details.nowIntegral"/>
<ai-info-item label="凭证" isLine v-if="fileDownLoad.length">
<ai-file-list :fileList="fileDownLoad" style="width: 200px;" :fileOps="{name: 'name'}"></ai-file-list>
</ai-info-item>
</ai-wrapper>
</template>
</ai-detail>
<span slot="footer" class="dialog-footer" center>
<el-button @click="dialog = false" style="width: 92px">关闭</el-button>
</span>
</el-dialog>
<ai-dialog :visible.sync="dialogDate" title="选择时间" width="500px" customFooter>
<el-date-picker v-model="timeList" size="small" type="daterange" value-format="yyyy-MM-dd"
range-separator="" start-placeholder="开始日期" end-placeholder="结束日期">
</el-date-picker>
<el-button slot="footer" @click="selectDete" type="primary">确认</el-button>
</ai-dialog>
</section>
</template>
<script>
import { mapState } from "vuex"
import * as echarts from 'echarts';
export default {
name: "gridScoreStatistics",
label: "积分统计",
props: {
instance: Function,
dict: Object,
permissions: Function,
},
data() {
return {
myChart1: null,
myChart2: null,
tableData: [],
search: {
current: 1,
userName: '',
girdId: '',
integralType: '',
startTime: '',
endTime: '',
},
girdIdArr:[],
total: 0,
size: 10,
current: 1,
girdList: [],
time: [],
timeCheck: ['昨日','近7天','近30天','自定义'],
dialog: false,
dialogDate: false,
timeList: [],
type: '1',
startTime: '',
endTime: '',
data: {},
girdId: '',
girdArr: [],
girdOptions: [],
defaultProps: {
label: 'girdName',
value: 'id',
children: 'children',
checkStrictly: true,
},
details: {},
fileDownLoad: [],
userSortListX: [],
userSortListY: [],
girdSortListX: [],
girdSortListY: [],
}
},
computed: {
...mapState(['user']),
colConfigs() {
return [
{ prop: "integralUserName", label: '姓名', align: "left", width: "200px" },
{ prop: "girdName", label: '所属网格', align: "center", width: "180px" },
{ slot: "eventDesc"},
{ slot: "integralType", label: '类型' },
{ slot: "changeIntegral", label: '积分变动', align: "center", },
{ prop: "nowIntegral", label: '剩余积分', align: "center", },
{ prop: "createTime", label: '时间', align: "center", },
{ slot: "options" }
]
}
},
created() {
this.$dict.load('epidemicDangerousAreaLevel','integralType','integralRuleEvent','integralRuleEventType').then(() => {
this.getStatistics()
this.getGridList()
this.getRanking()
this.getTableData()
})
},
methods: {
// 统计接口
getStatistics() {
this.instance.post('/app/appintegraluser/allGirdIntegral',null, {
params: {
type: this.type,
girdId: this.girdId,
startTime: this.startTime,
endTime: this.endTime,
}
}).then(res => {
if(res?.data) {
this.data = res.data
}
})
},
// 人员、网格排行
getRanking() {
this.instance.post('/app/appintegraluser/userAndGirdIntegralSort',null,{
params: {
type: this.type,
girdId: this.girdId,
startTime: this.startTime,
endTime: this.endTime
}
}).then((res) => {
if(res?.data) {
this.userSortListX = res.data.userSortList.map(e=> e.userName).reverse()
this.userSortListY = res.data.userSortList.map(e=> e.changeIntegral).reverse()
this.girdSortListX = res.data.girdSortList.map(e=> e.girdName).reverse()
this.girdSortListY = res.data.girdSortList.map(e=> e.changeIntegral).reverse()
this.getColEcherts1(this.userSortListX,this.userSortListY)
this.getColEcherts2(this.girdSortListX,this.girdSortListY)
}
})
},
// 积分明细
getTableData() {
this.instance.post('/app/appintegraluser/girdIntegralDetail',null,{
params: {
...this.search,
current: this.current,
size: this.size,
total: this.total,
}
}).then(res => {
if(res?.data) {
this.tableData = res.data.records
this.total = res.data.total
}
})
},
gridChangeOpt(val) {
this.girdIdArr = val
this.search.girdId = val?.[val.length - 1]
this.$refs.cascader2.dropDownVisible = false;
this.getTableData()
},
getColEcherts1(xData,yData) {
let chartDom1 = document.getElementById('chart1');
chartDom1.style.width = (window.innerWidth - 435) / 2 + "px";
this.myChart1 = echarts.init(chartDom1);
this.myChart1.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '16px',
right: '28px',
bottom: '14px',
top: '16px',
containLabel: true
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01],
},
yAxis: {
type: 'category',
data: xData,
axisTick: {
show: false,
},
axisLine: {
show: false,
},
},
series: [
{
data: yData,
type: 'bar',
itemStyle: {
normal: {
color: "#5087ec",
label: {
show: true, //开启显示
position: 'right', //在上方显示
textStyle: {
fontSize: 13,
color: '#666'
}
},
},
},
barWidth: 10,
barGap: '20%',
}
]
}, true);
window.addEventListener("resize", this.onResize)
},
getColEcherts2(xData,yData) {
let chartDom2 = document.getElementById('chart2');
chartDom2.style.width = (window.innerWidth - 435) / 2 + "px";
this.myChart2 = echarts.init(chartDom2);
this.myChart2.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '16px',
right: '28px',
bottom: '14px',
top: '16px',
containLabel: true
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01],
},
yAxis: {
type: 'category',
data: xData,
axisTick: {
show: false,
},
axisLine: {
show: false,
},
triggerEvent: true,
//设置文本过长超出隐藏...表示
axisLabel:{
margin: 8,
formatter: function(params){
var val=""
if(params.length > 8) {
val = params.substr(0,8)+'...'
return val
} else {
return params;
}
}
},
},
series: [
{
data: yData,
type: 'bar',
itemStyle: {
normal: {
color: "#5087ec",
label: {
show: true, //开启显示
position: 'right', //在右方显示
textStyle: {
fontSize: 13,
color: '#666'
}
},
},
},
barWidth: 10,
barGap: '20%',
}
]
}, true);
window.addEventListener("resize", this.onResize2)
// this.extension(this.myChart2)
},
onResize1() {
this.myChart1.resize()
},
onResize2() {
this.myChart2.resize()
},
gridChange(val) {
this.girdArr = val
this.girdId = val?.[val.length - 1]
this.$refs.cascader1.dropDownVisible = false;
this.getStatistics()
this.getRanking()
},
// 所有网格
getGridList() {
this.instance.post(`/app/appgirdinfo/listAll3`).then((res) => {
if (res?.code == 0) {
this.girdOptions = this.toTree(res.data)
}
})
},
// 转树形结构
toTree(data) {
let result = [];
if (!Array.isArray(data)) {
return result
}
let map = {};
data.forEach(item => {
map[item.id] = item;
});
data.forEach(item => {
let parent = map[item.parentGirdId];
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
},
timeChange(index) {
if(index == 3) {
this.dialogDate = true
}
this.type = index
this.getStatistics()
this.getRanking()
},
open(id) {
this.dialog = true
this.getDetail(id)
},
onChange(val) {
this.search.startTime = val?.[0]
this.search.endTime = val?.[1]
this.getTableData()
},
getDetail(id) {
this.instance.post(`/app/appintegraldetail/queryDetailById?id=${id}`).then(res=> {
if(res?.data) {
this.details = res.data
if(res.data.enclosure) {
let str = res.data.enclosure.split('/')
this.fileDownLoad = [{
url:res.data.enclosure,
name: str?.[str.length - 1]
}]
}
}
})
},
selectDete() {
if(!this.timeList || !this.timeList.length) {
return this.$message.error('请选择自定义时间');
}
this.startTime = this.timeList?.[0]
this.endTime = this.timeList?.[1]
this.dialogDate = false
this.getStatistics()
this.getRanking()
},
},
filters: {
formatTime(num) {
if(num > 0) {
return '+' + num
} else {
return num
}
}
},
mounted() {
this.getColEcherts1()
this.getColEcherts2()
},
destroyed () {
window.removeEventListener('resize', this.onResize1)
window.removeEventListener('resize', this.onResize2)
},
}
</script>
<style lang="scss" scoped>
.gridScoreStatistics {
height: 100%;
box-sizing: border-box;
padding-top: 20px;
.overallStatistics {
width: 100%;
margin-bottom: 16px;
padding: 20px;
box-sizing: border-box;
background: #FFF;
.title {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 16px;
p {
font-size: 16px;
font-family: MicrosoftYaHeiSemibold;
color: #222222;
font-weight: 600;
}
.title_right {
display: flex;
align-items: center;
span {
display: inline-block;
width: 70px;
height: 32px;
line-height: 32px;
border-radius: 2px;
border: 1px solid #D0D4DC;
margin-right: 8px;
text-align: center;
cursor: pointer;
}
.active {
color: #2266FF;
border: 1px solid #2266FF;
}
}
}
.card_list {
display: flex;
.card {
flex: 1;
height: 96px;
background: #F9F9F9;
border-radius: 2px;
margin-right: 20px;
padding: 16px 24px;
box-sizing: border-box;
h2 {
color: #888888;
font-weight: 600;
font-size: 16px;
}
p {
margin-top: 8px;
font-size: 24px;
font-weight: 600;
}
.color1 {
color: #2891FF;
}
.color2 {
color: #22AA99;
}
.color3 {
color: #F8B425;
}
}
.card:last-child {
margin-right: 0;
}
}
.echertsBox {
width: 100%;
margin-top: 20px;
background: #FFF;
display: flex;
.left_Box {
margin-right: 16px;
flex: 1;
}
.right_Box {
width: 100%;
flex: 1;
}
.left_Box,
.right_Box {
background: #F9F9F9;
box-shadow: 0px 4px 6px -2px rgba(15,15,21,0.1500);
border-radius: 4px;
padding: 16px;
box-sizing: border-box;
#chart1,
#chart2 {
width: 100%;
height: 300px;
}
p {
font-weight: 600;
}
}
}
}
// .chartCss {
// position: absolute;
// color: black;
// background:white;
// font-family: Aril;
// font-size: 12px;
// padding: 5px;
// display: inline;
// }
::v-deep .el-dialog__footer {
text-align: center;
}
::v-deep .el-dialog__header {
border-bottom: 1px solid #DDD;
}
::v-deep .ai-detail {
background: #FFF;
}
}
</style>

View File

@@ -103,10 +103,8 @@ export default {
return [
{prop: 'doTime', label: '时间', width: 200},
{prop: "type", label: "类型", dict: "integralDetailType", align: 'center'},
{
prop: 'changeIntegral', align: 'center', label: '变动积分',
render: (h, {row}) => h('p', `${row.integralCalcType == 1 ? '+' : '-'}${row.changeIntegral}`)
},
{prop: 'changeIntegral', align: 'center', label: '变动积分',render:
(h, {row}) => h('p', `${row.integralCalcType == 1 ? '+' : '-'}${row.changeIntegral}`)},
{prop: 'nowIntegral', align: 'center', label: '剩余积分'},
{prop: 'eventDesc', label: '事件', width: 500}
]

View File

@@ -17,7 +17,10 @@
<el-input v-model="search.name" size="mini" placeholder="设备名称" prefix-icon="el-icon-search"
@change="handleTreeFilter" clearable/>
</div>
<div title>设备列表</div>
<div title>
<div>设备列表</div>
<el-button type="text" icon="iconfont iconResetting" @click="updateDev" size="mini" :loading="btnLoading">刷新</el-button>
</div>
<div fill class="deviceList">
<el-scrollbar>
<el-tree ref="deviceTree" :data="treeData" :props="propsConfig" @node-click="handleNodeClick"
@@ -79,7 +82,8 @@ export default {
name: '',
search: {
bind: ''
}
},
btnLoading: false,
}
},
methods: {
@@ -98,6 +102,17 @@ export default {
}
})
},
updateDev() {
this.btnLoading = true
this.ins.post(`/app/appzyvideoequipment/sync`, null, {
timeout: 1000000
}).then(res => {
if (res.code == 0) {
this.$message.success('更新成功')
this.getDevices()
}
}).finally(() => this.btnLoading = false)
},
handleNodeClick(data) {
this.$emit('select', data)
},
@@ -106,13 +121,13 @@ export default {
return !this.search.bind ? true : data.deviceStatus === this.search.bind
}
return data?.name?.indexOf(v) > -1 && (!this.search.bind ? true : data.deviceStatus === this.search.bind)
return data?.name?.indexOf(v) > -1 && (!this.search.bind ? true : data.deviceStatus === this.search.bind)
},
handleTreeFilter(v) {
this.$refs.deviceTree?.filter(v)
},
onChange () {
onChange() {
this.$refs.deviceTree?.filter(this.search.name)
}
},
@@ -173,6 +188,19 @@ export default {
background: #3E4A69;
padding: 0 16px;
line-height: 28px;
display: flex;
justify-content: space-between;
align-items: center;
::v-deep .el-button {
padding: 0 4px;
height: 28px;
background: #3E4A69;
}
::v-deep .el-button:hover {
border: none;
}
}
::v-deep.deviceList {

View File

@@ -92,9 +92,8 @@
</el-button>
<ai-import :instance="instance" :dict="dict" v-if="tabIndex === 0" type="wxcp/wxuser" name="内部通讯录"
:importParams="{departmentId:search.departmentId}" @success="getList"/>
<el-button size="small" icon="iconfont iconUpdate_Files" v-if="tabIndex === 0" :loading="btnLoading"
@click="syncMembers">同步数据
</el-button>
<el-button size="small" icon="iconfont iconUpdate_Files" v-if="tabIndex === 0" :loading="btnLoading" @click="syncMembers">同步部门</el-button>
<el-button size="small" icon="iconfont iconUpdate_Files" v-if="tabIndex === 0" :loading="btnLoading" @click="syncUser">同步成员</el-button>
<ai-wechat-selecter refs="addTags" :instance="instance" v-model="users" @change="onChooseUser"
:disabled="currIndex < 0" v-if="tabIndex === 1">
<el-button size="small" :disabled="currIndex < 0" type="primary" icon="iconfont iconAdd">添加成员</el-button>
@@ -236,7 +235,7 @@ export default {
{prop: 'departmentNames', label: '部门'},
{prop: 'mobile', label: '手机号'},
{slot: 'tags', label: '标签'},
{prop: 'status', label: '账号状态', align: 'center', formart: v => v === 1 ? '已激活' : '未激活'}
{prop: 'status', label: '账号状态', align: 'center', formart: v => v == 1 ? '已激活' : '未激活'}
],
defaultProps: {
children: 'children',
@@ -505,25 +504,13 @@ export default {
},
syncMembers() {
let departId = this.search.departmentId;
if (!departId) departId = 1;
this.btnLoading = true
this.instance.post(`/app/wxcp/wxdepartment/syncDepart`).then(res => {
if (res.code == 0) {
this.instance.post(`/app/wxcp/wxdepartment/syncUser?departmentId=${departId}`, null, {
timeout: 1000000
}).then(res => {
if (res.code == 0) {
this.$message.success('同步成功')
this.getList()
this.getTree()
}
this.btnLoading = false
}).catch(() => {
this.btnLoading = false
})
this.$message.success('同步成功')
this.getList()
this.getTree()
}
}).catch(() => {
@@ -531,6 +518,26 @@ export default {
})
},
syncUser() {
let departId = this.search.departmentId;
if (!departId) departId = 1;
this.btnLoading = true
this.instance.post(`/app/wxcp/wxdepartment/syncUser?departmentId=${departId}`, null, {
timeout: 1000000
}).then(res => {
if (res.code == 0) {
this.$message.success('同步成功')
this.getList()
this.getTree()
}
this.btnLoading = false
}).catch(() => {
this.btnLoading = false
})
},
getTags() {
this.instance.post(`/app/wxcp/wxtag/listAll`).then(res => {
if (res.code == 0) {

View File

@@ -0,0 +1,44 @@
<template>
<section class="AiProcess">
<ai-dialog-btn :text="label">
<ai-workflow v-model="process" readonly/>
</ai-dialog-btn>
</section>
</template>
<script>
export default {
name: "AiProcess",
props: {
label: {default: "查看进度"},
bid: {default: "", required: true},
instance: Function
},
data() {
return {
detail: {},
process: {}
}
},
methods: {
getProcess() {
const {bid} = this.$props
bid && this.instance.post("", null, {
params: {bid}
}).then(res => {
if (res?.data) {
this.detail = res.data
}
})
}
},
created() {
this.getProcess()
}
}
</script>
<style lang="scss" scoped>
.AiProcess {
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<section class="AiWorkflow">
<div ref="lfIns" :style="{height}"/>
</section>
</template>
<script>
export default {
name: "AiWorkflow",
model: {
prop: "config",
event: "change"
},
props: {
config: Object,
height: {default: '400px'},
readonly: Boolean
},
computed: {
dndPanel: () => [
{
type: 'bpmn:startEvent',
text: '开始',
label: '开始',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==',
},
{
type: 'bpmn:userTask',
label: '流程节点',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==',
className: 'important-node'
},
{
type: 'bpmn:exclusiveGateway',
label: '条件判断',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAAHeEJUAAAAABGdBTUEAALGPC/xhBQAAAvVJREFUOBGNVEFrE0EU/mY3bQoiFlOkaUJrQUQoWMGePLX24EH0IIoHKQiCV0G8iE1covgLiqA/QTzVm1JPogc9tIJYFaQtlhQxqYjSpunu+L7JvmUTU3AgmTfvffPNN++9WSA1DO182f6xwILzD5btfAoQmwL5KJEwiQyVbSVZ0IgRyV6PTpIJ81E5ZvqfHQR0HUOBHW4L5Et2kQ6Zf7iAOhTFAA8s0pEP7AXO1uAA52SbqGk6h/6J45LaLhO64ByfcUzM39V7ZiAdS2yCePPEIQYvTUHqM/n7dgQNfBKWPjpF4ISk8q3J4nB11qw6X8l+FsF3EhlkEMfrjIer3wJTLwS2aCNcj4DbGxXTw00JmAuO+Ni6bBxVUCvS5d9aa04+so4pHW5jLTywuXAL7jJ+D06sl82Sgl2JuVBQn498zkc2bGKxULHjCnSMadBKYDYYHAtsby1EQ5lNGrQd4Y3v4Zo0XdGEmDno46yCM9Tk+RiJmUYHS/aXHPNTcjxcbTFna000PFJHIVZ5lFRqRpJWk9/+QtlOUYJj9HG5pVFEU7zqIYDVsw2s+AJaD8wTd2umgSCCyUxgGsS1Y6TBwXQQTFuZaHcd8gAGioE90hlsY+wMcs30RduYtxanjMGal8H5dMW67dmT1JFtYUEe8LiQLRsPZ6IIc7A4J5tqco3T0pnv/4u0kyzrYUq7gASuEyI8VXKvB9Odytv6jS/PNaZBln0nioJG/AVQRZvApOdhjj3Jt8QC8Im09SafwdBdvIpztpxWxpeKCC+EsFdS8DCyuCn2munFpL7ctHKp+Xc5cMybeIyMAN33SPL3ZR9QV1XVwLyzHm6Iv0/yeUuUb7PPlZC4D4HZkeu6dpF4v9j9MreGtMbxMMRLIcjJic9yHi7WQ3yVKzZVWUr5UrViJvn1FfUlwe/KYVfYyWRLSGNu16hR01U9IacajXPei0wx/5BqgInvJN+MMNtNme7ReU9SBbgntovn0kKHpFg7UogZvaZiOue/q1SBo9ktHzQAAAAASUVORK5CYII=',
},
{
type: 'bpmn:endEvent',
text: '结束',
label: '结束',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAA1BJREFUOBFtVE1IVUEYPXOf+tq40Y3vPcmFIdSjIorWoRG0ERWUgnb5FwVhYQSl72oUoZAboxKNFtWiwKRN0M+jpfSzqJAQclHo001tKkjl3emc8V69igP3znzfnO/M9zcDcKT67azmjYWTwl9Vn7Vumeqzj1DVb6cleQY4oAVnIOPb+mKAGxQmKI5CWNJ2aLPatxWa3aB9K7/fB+/Z0jUF6TmMlFLQqrkECWQzOZxYGjTlOl8eeKaIY5yHnFn486xBustDjWT6dG7pmjHOJd+33t0iitTPkK6tEvjxq4h2MozQ6WFSX/LkDUGfFwfhEZj1Auz/U4pyAi5Sznd7uKzznXeVHlI/Aywmk6j7fsUsEuCGADrWARXXwjxWQsUbIupDHJI7kF5dRktg0eN81IbiZXiTESic50iwS+t1oJgL83jAiBupLDCQqwziaWSoAFSeIR3P5Xv5az00wyIn35QRYTwdSYbz8pH8fxUUAtxnFvYmEmgI0wYXUXcCCSpeEVpXlsRhBnCEATxWylL9+EKCAYhe1NGstUa6356kS9NVvt3DU2fd+Wtbm/+lSbylJqsqkSm9CRhvoJVlvKPvF1RKY/FcPn5j4UfIMLn8D4UYb54BNsilTDXKnF4CfTobA0FpoW/LSp306wkXM+XaOJhZaFkcNM82ASNAWMrhrUbRfmyeI1FvRBTpN06WKxa9BK0o2E4Pd3zfBBEwPsv9sQBnmLVbLEIZ/Xe9LYwJu/Er17W6HYVBc7vmuk0xUQ+pqxdom5Fnp55SiytXLPYoMXNM4u4SNSCFWnrVIzKG3EGyMXo6n/BQOe+bX3FClY4PwydVhthOZ9NnS+ntiLh0fxtlUJHAuGaFoVmttpVMeum0p3WEXbcll94l1wM/gZ0Ccczop77VvN2I7TlsZCsuXf1WHvWEhjO8DPtyOVg2/mvK9QqboEth+7pD6NUQC1HN/TwvydGBARi9MZSzLE4b8Ru3XhX2PBxf8E1er2A6516o0w4sIA+lwURhAON82Kwe2iDAC1Watq4XHaGQ7skLcFOtI5lDxuM2gZe6WFIotPAhbaeYlU4to5cuarF1QrcZ/lwrLaCJl66JBocYZnrNlvm2+MBCTmUymPrYZVbjdlr/BxlMjmNmNI3SAAAAAElFTkSuQmCC',
}]
},
data() {
return {
flow: null,
configWatch: null
}
},
methods: {
loadLib() {
this.$injectCss("https://cdn.cunwuyun.cn/logicflow/index.css")
const load = url => new Promise(resolve => this.$injectLib(url, () => resolve()))
let libs = ["https://cdn.cunwuyun.cn/logicflow/logic-flow.js", "https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/BpmnElement.js"]
if (!this.readonly) {
this.$injectCss("https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/style/index.css")
libs = [
libs,
"https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/Menu.js",
"https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/DndPanel.js"
].flat()
}
return Promise.all(libs.map(e => load(e)))
},
initFlow(count = 0) {
const {LogicFlow, Menu, DndPanel, BpmnElement} = window
let plugins = [BpmnElement, this.readonly ? [] : [Menu, DndPanel]].flat()
if (!!LogicFlow && this.$refs.lfIns && plugins.reduce((r, e) => r && !!e, true)) {
this.flow = new LogicFlow({container: this.$refs.lfIns, plugins})
this.flow.extension.dndPanel?.setPatternItems(this.dndPanel)
this.initValue()
this.flow.on('history:change', evt => {
this.configWatch?.()
const conf = this.$copy(evt.data?.undos || null).pop()
this.$emit("change", conf)
})
} else if (count < 10) {
setTimeout(() => this.initFlow(++count), 200)
} else console.error("logicFlow加载失败!")
},
initValue() {
this.configWatch = this.$watch('config', v => {
if (v?.nodes?.length > 0) {
this.flow?.render(v || {})
this.configWatch?.()
}
}, {immediate: true, deep: true})
}
},
mounted() {
this.$nextTick(() => this.loadLib().then(() => this.initFlow()))
}
}
</script>
<style lang="scss" scoped>
.AiWorkflow {
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<section class="AppWorkflowManage">
<component :is="currentPage" v-bind="$props"/>
</section>
</template>
<script>
import List from "./list";
import Add from "./add";
import WorkflowLogs from "./workflowLogs";
export default {
name: "AppWorkflowManage",
components: {WorkflowLogs, Add, List},
label: "工作流管理",
props: {
instance: Function,
dict: Object,
permissions: Function
},
computed: {
currentPage() {
let {hash} = this.$route
return hash == "#add" ? Add :
hash == "#logs" ? WorkflowLogs : List
}
},
created() {
this.dict.load('yesOrNo')
}
}
</script>
<style lang="scss" scoped>
.AppWorkflowManage {
height: 100%;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<section class="add">
<ai-detail>
<ai-title slot="title" :title="pageTitle" isShowBottomBorder/>
<template #content>
<el-form ref="AddForm" :model="form" size="small" label-width="120px" :rules="rules">
<ai-card title="基础设置">
<template #content>
<el-row type="flex">
<el-form-item label="流程名称" prop="name" class="fill">
<el-input v-model="form.name" placeholder="请输入流程名称" clearable/>
</el-form-item>
<el-form-item label="对应应用" prop="app" class="fill">
<el-input v-model="form.app" placeholder="请输入对应应用" clearable/>
</el-form-item>
</el-row>
</template>
</ai-card>
<ai-card title="流程设计">
<template #content>
<ai-workflow v-model="form.config"/>
</template>
</ai-card>
</el-form>
</template>
<template #footer>
<el-button @click="back">取消</el-button>
<el-button type="primary" @click="submit">提交</el-button>
</template>
</ai-detail>
</section>
</template>
<script>
import AiWorkflow from "./AiWorkflow";
import {mapActions} from "vuex"
export default {
name: "add",
components: {AiWorkflow},
props: {
instance: Function,
dict: Object,
permissions: Function
},
computed: {
isEdit: v => !!v.$route.query.id,
pageTitle: v => v.isEdit ? "编辑工作流管理" : "新增工作流管理"
},
data() {
return {
form: {},
rules: {
name: {required: true, message: "请输入"}, app: {required: true, message: "请输入"},
},
}
},
methods: {
...mapActions(['getWorkflowConfigs']),
getDetail() {
let {id} = this.$route.query
id && this.instance.post("/app/appworkflowmanage/queryDetailById", null, {
params: {id}
}).then(res => {
if (res?.data) {
const {config} = res.data
this.form = res.data
this.form.config = JSON.parse(config || null)
}
})
},
back() {
this.$router.push({})
},
submit() {
this.$refs.AddForm.validate(v => {
if (v) {
let {config} = this.form
config = JSON.stringify(config)
this.instance.post("/app/appworkflowmanage/addOrUpdate", {...this.form, config}).then(res => {
if (res?.code == 0) {
this.$message.success("提交成功!")
this.getWorkflowConfigs()
this.back()
}
})
}
})
},
},
created() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.add {
height: 100%;
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<section class="list">
<ai-list>
<ai-title slot="title" title="工作流管理" isShowBottomBorder/>
<template #content>
<ai-search-bar>
<template #left>
<el-button type="primary" icon="iconfont iconAdd" @click="handleAdd()">添加</el-button>
</template>
<template #right>
<el-input size="small" placeholder="搜索" v-model="search.name" clearable
@change="page.current=1,getTableData()"/>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="page.total" :current.sync="page.current" :size.sync="page.size"
@getList="getTableData" :col-configs="colConfigs" :dict="dict">
<el-table-column slot="options" label="操作" fixed="right" align="center" width="300">
<template slot-scope="{row}">
<el-button type="text" @click="handleAdd(row.id)">编辑</el-button>
<el-button type="text" @click="handleLogs(row.id)">台账</el-button>
<el-button type="text" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</ai-table>
</template>
</ai-list>
</section>
</template>
<script>
export default {
name: "list",
props: {
instance: Function,
dict: Object,
permissions: Function
},
data() {
return {
search: {name: ""},
page: {current: 1, size: 10, total: 0},
tableData: [],
colConfigs: [
{prop: "name", label: "流程名称"},
{prop: "app", label: "对应应用"},
{prop: "id", label: "流程id"}
],
}
},
methods: {
getTableData() {
this.instance.post("/app/appworkflowmanage/list", null, {
params: {...this.page, ...this.search}
}).then(res => {
if (res?.data) {
this.tableData = res.data.records
this.page.total = res.data.total
}
})
},
handleAdd(id) {
this.$router.push({hash: "#add", query: {id}})
},
handleLogs(id) {
this.$router.push({hash: "#logs", query: {id}})
},
handleDelete(ids) {
this.$confirm("是否要删除?").then(() => {
this.instance.post("/app/appworkflowmanage/delete", null, {
params: {ids}
}).then(res => {
if (res?.code == 0) {
this.$message.success("删除成功")
this.getTableData()
}
})
}).catch(() => 0)
}
},
created() {
this.getTableData()
}
}
</script>
<style lang="scss" scoped>
.list {
height: 100%;
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<section class="workflowLogs">
<ai-list>
<ai-title slot="title" title="流程台账" isShowBottomBorder isShowBack @back="back"/>
<template #content>
<ai-search-bar>
<template #right>
<el-input size="small" placeholder="搜索" v-model="search.name" clearable
@change="page.current=1,getTableData()"/>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="page.total" :current.sync="page.current" :size.sync="page.size"
@getList="getTableData" :col-configs="colConfigs" :dict="dict">
<el-table-column slot="options" label="操作" fixed="right" align="center" width="300">
<template slot-scope="{row}">
<el-button type="text" @click="showProcess(row.workflowConfig)">查看进度</el-button>
</template>
</el-table-column>
</ai-table>
</template>
</ai-list>
<ai-dialog :visible.sync="dialog" title="查看进度" @closed="process=null" customFooter>
<ai-workflow v-model="process" readonly/>
<template #footer>
<el-button @click="dialog=false">关闭</el-button>
</template>
</ai-dialog>
</section>
</template>
<script>
import AiWorkflow from "./AiWorkflow";
export default {
name: "workflowLogs",
components: {AiWorkflow},
props: {
instance: Function,
dict: Object,
permissions: Function
},
data() {
return {
search: {name: ""},
page: {current: 1, size: 10, total: 0},
tableData: [],
colConfigs: [
{prop: "pid", label: "流程ID"},
{prop: "bid", label: "业务ID"},
{prop: "createUserName", label: "创建者"},
],
dialog: false,
process: null
}
},
methods: {
getTableData() {
this.instance.post("/app/appworkflowlog/list", null, {
params: {...this.page, ...this.search}
}).then(res => {
if (res?.data) {
this.tableData = res.data.records
this.page.total = res.data.total
}
})
},
back() {
this.$router.push({})
},
showProcess(process) {
this.process = JSON.parse(process)
this.dialog = true
}
},
created() {
this.getTableData()
}
}
</script>
<style lang="scss" scoped>
.workflowLogs {
height: 100%;
}
</style>

View File

@@ -300,7 +300,7 @@ export default {
},
//map搜索
confirm(row, points) {
this.instance.post(`/app/appgirdinfo/addOrUpdate`, {...row, points}).then((res) => {
this.instance.post(`/app/appgirdinfo/updateCoordinate`, {...row, points}).then((res) => {
if (res.code == 0) {
this.$message.success("提交成功!")
this.getList();

View File

@@ -1,8 +1,7 @@
<template>
<section class="AppResident">
<ai-list v-if="!showDetail" isTabs>
<ai-title slot="title" title="居民档案" :instance="instance" :hideLevel="hideLevel-1" isShowArea
v-model="areaId"/>
<ai-title slot="title" title="居民档案" :instance="instance" :hideLevel="hideLevel-1" :isShowArea="permissions('app_datastatistics')" v-model="areaId"/>
<template #tabs>
<el-tabs v-model="activeName">
<el-tab-pane v-for="op in tabs" :key="op.value" :name="op.value" :label="op.label">
@@ -67,14 +66,14 @@ export default {
comp: ResidentList,
detail: ResidentDetail
})),
{label: "居民统计", value: "3", comp: ResidentSta},
{label: "居民档案审核", value: "4", comp: auditList, detail: auditDetail}
]
{label: "居民统计", value: "3", comp: ResidentSta, permit: "app_datastatistics"},
{label: "居民档案审核", value: "4", comp: auditList, detail: auditDetail, permit: "app_appresident_examine"}
].filter(e => !e.permit || this.permissions(e.permit))
}
},
created() {
this.activeName = this.$route.query?.type
this.areaId = this.$copy(this.user.info.areaId)
this.areaId = this.permissions("app_datastatistics") ? this.$copy(this.user.info.areaId) : ""
this.dict.load('residentType', "sex", "faithType", "fileStatus", "legality", "education", "maritalStatus",
"politicsStatus", "householdName", "nation", "liveReason", "certificateType", "job", "militaryStatus",
"householdRelation", "logoutReason", "nation", "registerStatus", "residentTipType", "liveCategory",

View File

@@ -49,7 +49,7 @@ export default {
permissions: Function
},
computed: {
...mapState(['user']),
...mapState(['user', 'sys']),
rules() {
return {
labelName: [{required: true, message: "请输入标签"}],
@@ -57,7 +57,12 @@ export default {
},
dialogTitle() {
return `${this.form.id ? "编辑" : "添加"}标签`
}
},
colConfigs: v => [{type: "selection"},
{label: "标签信息", prop: "labelName"},
{label: "创建时间", prop: "createTime", align: '120px'},
{label: "创建人", prop: "createUserName", align: 'center', openType: v.$sys?.edition == "saas" ? "userName" : null},
{slot: "options"}]
},
data() {
return {
@@ -66,13 +71,6 @@ export default {
tableData: [],
search: {name: "", ids: ""},
form: {},
colConfigs: [
{type: "selection"},
{label: "标签信息", prop: "labelName"},
{label: "创建时间", prop: "createTime", align: '120px'},
{label: "创建人", prop: "createUserName", align: 'center'},
{slot: "options"}
]
}
},
methods: {

View File

@@ -202,7 +202,8 @@
</ai-card>
<tags-manage v-if="currentTab=='0'&&baseInfo.id&&permissions('app_appresidentlabelinfo_detail')" v-bind="$props" :resident-id="baseInfo.id"/>
</el-tab-pane>
<el-tab-pane label="资产信息" lazy>
<!--暂时用审核的权限码控制,后端没时间加-->
<el-tab-pane label="资产信息" lazy v-if="permissions('app_appresident_examine')">
<personal-assets v-if="currentTab==1&&baseInfo.id" :resident-id="baseInfo.id" v-bind="$props"/>
</el-tab-pane>
<el-tab-pane label="特殊人群" lazy v-if="hasSpecial">

View File

@@ -19,68 +19,34 @@
<ai-select placeholder="民族" v-model="search.nation"
:selectList="resident.dict.getDict('nation')"
@change="page.current=1,refreshTable()"/>
<el-date-picker
value-format="yyyy-MM-dd HH:mm:ss"
v-model="search.birthStart"
style="width:250px;border-radius:0;"
type="date"
size="small"
unlink-panels
placeholder="选择出生开始日期"
@change="page.current=1,refreshTable()"
/>
<el-date-picker
value-format="yyyy-MM-dd HH:mm:ss"
v-model="search.birthEnd"
style="width:250px;border-radius:0;"
type="date"
size="small"
placeholder="选择出生结束日期"
unlink-panels
@change="page.current=1,refreshTable()"
/>
<el-select
v-model="search.politicsStatus"
placeholder="政治面貌"
size="small"
@change="page.current=1,refreshTable()"
clearable
>
<el-option
v-for="(item,i) in resident.dict.getDict('politicsStatus')"
:key="i"
:label="item.dictName"
:value="item.dictValue"
></el-option>
</el-select>
<el-select
v-model="search.householdName"
placeholder="是否户主"
size="small"
@change="page.current=1,refreshTable()"
clearable
>
<el-option
v-for="(item,i) in resident.dict.getDict('householdName')"
:key="i"
:label="item.dictName"
:value="item.dictValue"
></el-option>
</el-select>
<el-select
v-model="search.faithType"
placeholder="宗教信仰"
@change="page.current=1,refreshTable()"
size="small"
clearable
>
<el-option
v-for="(item,i) in resident.dict.getDict('faithType')"
:key="i"
:label="item.dictName"
:value="item.dictValue"
></el-option>
</el-select>
<ai-search label="出生日期">
<el-date-picker
value-format="yyyy-MM-dd HH:mm:ss"
v-model="search.birthStart"
type="date"
size="small"
placeholder="选择开始日期"
@change="page.current=1,refreshTable()"
/>
<el-date-picker
value-format="yyyy-MM-dd HH:mm:ss"
v-model="search.birthEnd"
type="date"
size="small"
placeholder="选择结束日期"
@change="page.current=1,refreshTable()"
/>
</ai-search>
<ai-select placeholder="政治面貌" v-model="search.politicsStatus"
:selectList="resident.dict.getDict('politicsStatus')"
@change="page.current=1,refreshTable()"/>
<ai-select placeholder="是否户主" v-model="search.householdName"
:selectList="resident.dict.getDict('householdName')"
@change="page.current=1,refreshTable()"/>
<ai-select placeholder="宗教信仰" v-model="search.faithType"
:selectList="resident.dict.getDict('faithType')"
@change="page.current=1,refreshTable()"/>
</template>
<template #right>
<el-input
@@ -232,30 +198,6 @@ export default {
};
},
methods: {
handleClick() {
this.tableData = [];
this.multipleSelection = [];
this.searchInit()
},
searchInit() {
let tempAreaId = this.search.areaId;
this.search = {
fileStatus: "",
sex: "",
nation: "",
education: "",
politicsStatus: "",
birth: [],
faithType: "",
householdName: "",
areaId: "",
con: "",
maritalStatus: ""
};
this.search.areaId = tempAreaId;
this.page = {current: 1, size: 10, total: 0};
this.refreshTable()
},
handleSelectionChange(val) {
this.deleteIds = [];
this.multipleSelection = val;
@@ -263,41 +205,6 @@ export default {
this.deleteIds.push(e.id);
});
},
exportrExcle() {
if (this.deleteIds.length == 0) {
if (this.search.birth) {
this.search.birth = this.search.birth.join(",");
}
this.resident.instance
.post(`/app/appresident/exportAll`, null, {
params: {
...this.search,
...this.page
}
})
.then(res => {
if (res && res.code == 0) {
this.$message.success(res.data);
if (typeof this.search.birth == "string") {
this.search.birth = this.search.birth.split(",");
}
}
});
} else {
this.resident.instance.post(`/app/appresident/exportByIds`, {
ids: this.deleteIds,
areaId: this.user.info.areaId
}).then(res => {
if (res?.code == 0) {
this.$message.success(res.data);
}
});
}
},
handleSizeChange(val) {
this.page.size = val;
this.refreshTable()
},
detailShow(row) {
this.$router.push({query: {type: this.active, id: row.id}})
},
@@ -349,5 +256,15 @@ export default {
<style lang="scss" scoped>
.ResidentList {
height: 100%;
::v-deep.AiSearch {
.el-input + .el-input > .el-input__inner {
border-left-color: transparent;
&:hover, &:focus {
border-left-color: inherit;
}
}
}
}
</style>

View File

@@ -140,7 +140,8 @@ export default {
return true
}
})) {
const {cateList: categorys, moduleId} = this
let {cateList: categorys, moduleId} = this
categorys = categorys.map((e, i) => ({...e, showIndex: i * 1 + 1}))
this.instance.post(`/app/appcontentmodulecategory/addOrUpdate2`, {
categorys, moduleId
}).then(res => {

View File

@@ -1,73 +1,71 @@
<template>
<div class="doc-circulation ailist-wrapper">
<keep-alive :include="['List']">
<component ref="component" :is="component" @change="onChange" :params="params" :instance="instance" :dict="dict"></component>
<component ref="component" :is="component" @change="onChange" :params="params" v-bind="$props"/>
</keep-alive>
</div>
</template>
<script>
import List from './components/List'
import Add from './components/Add'
import Detail from './components/Detail'
import List from './components/List'
import Add from './components/Add'
import Detail from './components/Detail'
export default {
name: 'AppVillageIntroduction',
label: '本村简介',
export default {
name: 'AppVillageIntroduction',
label: '本村简介',
props: {
instance: Function,
dict: Object,
menuName: {default: "本村简介"}
},
data() {
return {
component: 'List',
params: {},
include: []
}
},
components: {
Add,
List,
Detail
},
props: {
instance: Function,
dict: Object
},
mounted() {
},
data () {
return {
component: 'List',
params: {},
include: []
methods: {
onChange(data) {
if (data.type === 'Add') {
this.component = 'Add'
this.params = data.params
}
},
components: {
Add,
List,
Detail
},
if (data.type === 'Detail') {
this.component = 'Detail'
this.params = data.params
}
mounted () {
},
if (data.type === 'list') {
this.component = 'List'
this.params = data.params
methods: {
onChange (data) {
if (data.type === 'Add') {
this.component = 'Add'
this.params = data.params
}
if (data.type === 'Detail') {
this.component = 'Detail'
this.params = data.params
}
if (data.type === 'list') {
this.component = 'List'
this.params = data.params
this.$nextTick(() => {
if (data.isRefresh) {
this.$refs.component.getList()
}
})
}
this.$nextTick(() => {
if (data.isRefresh) {
this.$refs.component.getList()
}
})
}
}
}
}
</script>
<style lang="scss">
.doc-circulation {
height: 100%;
background: #F3F6F9;
overflow: auto;
}
.doc-circulation {
height: 100%;
background: #F3F6F9;
overflow: auto;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<ai-detail>
<template slot="title">
<ai-title :title="params.id ? '编辑本村简介' : '添加本村简介'" isShowBack isShowBottomBorder @onBackClick="cancel(false)">
<ai-title :title="pageTitle" isShowBack isShowBottomBorder @onBackClick="cancel(false)">
</ai-title>
</template>
<template slot="content">
@@ -19,12 +19,12 @@
</el-form-item>
<el-form-item label="缩略图" prop="thumbUrl">
<ai-uploader
:instance="instance"
isShowTip
v-model="form.thumbUrl"
:limit="1"
:cropOps="cropOps"
is-crop>
:instance="instance"
isShowTip
v-model="form.thumbUrl"
:limit="1"
:cropOps="cropOps"
is-crop>
<template slot="tips">
<p>最多上传1张图片,单个文件最大10MB支持jpgjpegpng格式</p>
<p>图片比例1.61</p>
@@ -43,89 +43,91 @@
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'Add',
import {mapState} from 'vuex'
props: {
instance: Function,
dict: Object,
params: Object
},
export default {
name: 'Add',
data () {
return {
info: {},
form: {
title: '',
content: '',
areaId: '',
createUnitName: '',
createUserName: '',
status: '',
thumbUrl: []
},
cropOps: {
width: "336px",
height: "210px"
},
id: ''
}
},
props: {
instance: Function,
dict: Object,
params: Object,
menuName: String
},
computed: {
...mapState(['user'])
},
created () {
this.form.areaId = this.user.info.areaId
this.disabledLevel = this.user.info.areaList.length
if (this.params && this.params.id) {
this.id = this.params.id
this.getInfo(this.params.id)
}
},
methods: {
getInfo (id) {
this.instance.post(`/app/appcountrysidetourism/queryDetailById?id=${id}`).then(res => {
if (res.code === 0) {
this.form = res.data
this.form.thumbUrl = res.data.thumbUrl ? JSON.parse(res.data.thumbUrl) : []
}
})
data() {
return {
info: {},
form: {
title: '',
content: '',
areaId: '',
createUnitName: '',
createUserName: '',
status: '',
thumbUrl: []
},
confirm () {
this.$refs.form.validate((valid) => {
if (valid) {
this.instance.post(`/app/appcountrysidetourism/addOrUpdate`, {
...this.form,
type: 0,
createUserName: this.user.info.name,
thumbUrl: this.form.thumbUrl.length ? JSON.stringify([{
url: this.form.thumbUrl[0].url
}]) : ''
}).then(res => {
if (res.code == 0) {
this.$message.success('提交成功')
setTimeout(() => {
this.cancel(true)
}, 600)
}
})
}
})
cropOps: {
width: "336px",
height: "210px"
},
id: ''
}
},
cancel (isRefresh) {
this.$emit('change', {
type: 'list',
isRefresh: !!isRefresh
})
}
computed: {
...mapState(['user']),
pageTitle: v => `${!!v.params.id ? '编辑' : '添加'}${v.menuName}`
},
created() {
this.form.areaId = this.user.info.areaId
this.disabledLevel = this.user.info.areaList.length
if (this.params && this.params.id) {
this.id = this.params.id
this.getInfo(this.params.id)
}
},
methods: {
getInfo(id) {
this.instance.post(`/app/appcountrysidetourism/queryDetailById?id=${id}`).then(res => {
if (res.code === 0) {
this.form = res.data
this.form.thumbUrl = res.data.thumbUrl ? JSON.parse(res.data.thumbUrl) : []
}
})
},
confirm() {
this.$refs.form.validate((valid) => {
if (valid) {
this.instance.post(`/app/appcountrysidetourism/addOrUpdate`, {
...this.form,
type: 0,
createUserName: this.user.info.name,
thumbUrl: this.form.thumbUrl.length ? JSON.stringify([{
url: this.form.thumbUrl[0].url
}]) : ''
}).then(res => {
if (res.code == 0) {
this.$message.success('提交成功')
setTimeout(() => {
this.cancel(true)
}, 600)
}
})
}
})
},
cancel(isRefresh) {
this.$emit('change', {
type: 'list',
isRefresh: !!isRefresh
})
}
}
}
</script>
<style scoped lang="scss">

View File

@@ -1,7 +1,7 @@
<template>
<ai-list class="notice">
<template slot="title">
<ai-title title="本村简介" isShowBottomBorder isShowArea v-model="search.areaId" :instance="instance" @change="search.current = 1, getList()"></ai-title>
<ai-title :title="menuName" isShowBottomBorder isShowArea v-model="search.areaId" :instance="instance" @change="search.current = 1, getList()"></ai-title>
</template>
<template slot="content">
<ai-search-bar class="search-bar">
@@ -57,9 +57,9 @@
props: {
instance: Function,
dict: Object
dict: Object,
menuName:String
},
data() {
return {
search: {

View File

@@ -1,73 +1,74 @@
<template>
<div class="doc-circulation ailist-wrapper">
<keep-alive :include="['List']">
<component ref="component" :is="component" @change="onChange" :params="params" :instance="instance" :dict="dict"></component>
<component ref="component" :is="component" @change="onChange" :params="params" v-bind="$props"/>
</keep-alive>
</div>
</template>
<script>
import List from './components/List'
import Add from './components/Add'
import Detail from './components/Detail'
import List from './components/List'
import Add from './components/Add'
import Detail from './components/Detail'
export default {
name: 'AppVillageRegulations',
label: '村规民约',
export default {
name: 'AppVillageRegulations',
label: '村规民约',
props: {
instance: Function,
dict: Object
},
props: {
instance: Function,
dict: Object,
menuName: {default: "村规民约"}
},
data () {
return {
component: 'List',
params: {},
include: []
data() {
return {
component: 'List',
params: {},
include: []
}
},
components: {
Add,
List,
Detail
},
mounted() {
},
methods: {
onChange(data) {
if (data.type === 'Add') {
this.component = 'Add'
this.params = data.params
}
},
components: {
Add,
List,
Detail
},
if (data.type === 'Detail') {
this.component = 'Detail'
this.params = data.params
}
mounted () {
},
if (data.type === 'list') {
this.component = 'List'
this.params = data.params
methods: {
onChange (data) {
if (data.type === 'Add') {
this.component = 'Add'
this.params = data.params
}
if (data.type === 'Detail') {
this.component = 'Detail'
this.params = data.params
}
if (data.type === 'list') {
this.component = 'List'
this.params = data.params
this.$nextTick(() => {
if (data.isRefresh) {
this.$refs.component.getList()
}
})
}
this.$nextTick(() => {
if (data.isRefresh) {
this.$refs.component.getList()
}
})
}
}
}
}
</script>
<style lang="scss">
.doc-circulation {
height: 100%;
background: #F3F6F9;
overflow: auto;
}
.doc-circulation {
height: 100%;
background: #F3F6F9;
overflow: auto;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<ai-detail>
<template slot="title">
<ai-title :title="params.id ? '编辑村规民约' : '添加村规民约'" isShowBack isShowBottomBorder @onBackClick="cancel(false)">
<ai-title :title="pageTitle" isShowBack isShowBottomBorder @onBackClick="cancel(false)">
</ai-title>
</template>
<template slot="content">
@@ -50,7 +50,8 @@
props: {
instance: Function,
dict: Object,
params: Object
params: Object,
menuName: String
},
data () {
@@ -74,7 +75,8 @@
},
computed: {
...mapState(['user'])
...mapState(['user']),
pageTitle: v => `${!!v.params.id ? '编辑' : '添加'}${v.menuName}`
},
created () {

View File

@@ -1,7 +1,7 @@
<template>
<ai-list class="notice">
<template slot="title">
<ai-title title="村规民约" isShowBottomBorder isShowArea v-model="search.areaId" :instance="instance" @change="search.current = 1, getList()"></ai-title>
<ai-title :title="menuName" isShowBottomBorder isShowArea v-model="search.areaId" :instance="instance" @change="search.current = 1, getList()"></ai-title>
</template>
<template slot="content">
<ai-search-bar class="search-bar">
@@ -10,25 +10,25 @@
</template>
<template #right>
<el-input
v-model="search.title"
class="search-input"
size="small"
v-throttle="() => {search.current=1,getList()}"
placeholder="请输入标题"
clearable
@clear="search.current = 1, search.title = '', getList()"
suffix-icon="iconfont iconSearch">
v-model="search.title"
class="search-input"
size="small"
v-throttle="() => {search.current=1,getList()}"
placeholder="请输入标题"
clearable
@clear="search.current = 1, search.title = '', getList()"
suffix-icon="iconfont iconSearch">
</el-input>
</template>
</ai-search-bar>
<ai-table
:tableData="tableData"
:col-configs="colConfigs"
:total="total"
style="margin-top: 6px;"
:current.sync="search.current"
:size.sync="search.size"
@getList="getList">
:tableData="tableData"
:col-configs="colConfigs"
:total="total"
style="margin-top: 6px;"
:current.sync="search.current"
:size.sync="search.size"
@getList="getList">
<el-table-column slot="tags" label="标签">
<template slot-scope="{ row }">
<div class="table-tags">
@@ -51,101 +51,103 @@
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'List',
import {mapState} from 'vuex'
props: {
instance: Function,
dict: Object
export default {
name: 'List',
props: {
instance: Function,
dict: Object,
menuName: String
},
data() {
return {
search: {
current: 1,
size: 10,
title: '',
areaId: ''
},
currIndex: -1,
areaList: [],
total: 10,
colConfigs: [
{prop: 'title', label: '标题', align: 'left', width: '200px'},
{prop: 'areaName', label: '地区', align: 'center'},
{prop: 'status', label: '发布状态', align: 'center', formart: v => v === '1' ? '已发布' : '未发布'},
{prop: 'createUserName', label: '发布人', align: 'center'},
{prop: 'createDate', label: '发布时间', align: 'center'},
{slot: 'options', label: '操作', align: 'center'}
],
areaName: '',
unitName: '',
tableData: []
}
},
computed: {
...mapState(['user'])
},
mounted() {
this.search.areaId = this.user.info.areaId
this.getList()
},
methods: {
getList() {
this.instance.post(`/app/appcountrysidetourism/list`, null, {
params: {
type: 4,
...this.search
}
}).then(res => {
if (res.code == 0) {
this.tableData = res.data.records
this.total = res.data.total
}
})
},
data() {
return {
search: {
current: 1,
size: 10,
title: '',
areaId: ''
},
currIndex: -1,
areaList: [],
total: 10,
colConfigs: [
{ prop: 'title', label: '标题', align: 'left', width: '200px' },
{ prop: 'areaName', label: '地区', align: 'center' },
{ prop: 'status', label: '发布状态', align: 'center', formart: v => v === '1' ? '已发布' : '未发布' },
{ prop: 'createUserName', label: '发布人', align: 'center' },
{ prop: 'createDate', label: '发布时间', align: 'center' },
{ slot: 'options', label: '操作', align: 'center' }
],
areaName: '',
unitName: '',
tableData: []
}
},
computed: {
...mapState(['user'])
},
mounted() {
this.search.areaId = this.user.info.areaId
this.getList()
},
methods: {
getList() {
this.instance.post(`/app/appcountrysidetourism/list`, null, {
params: {
type: 4,
...this.search
changeStatus(item) {
let title = item.status == '1' ? '是否要取消发布?' : '是否要发布?';
this.$confirm(title, {type: 'warning'}).then(() => {
item.status = item.status == '1' ? '0' : '1'
this.instance.post('/app/appcountrysidetourism/addOrUpdate', item).then(res => {
if (res && res.code == 0) {
title == '是否要发布?' ? this.$message.success('发布成功') : this.$message.success('取消发布成功')
this.getList()
}
}).then(res => {
})
})
},
remove(id) {
this.$confirm('确定删除该数据?').then(() => {
this.instance.post(`/app/appcountrysidetourism/delete?ids=${id}`).then(res => {
if (res.code == 0) {
this.tableData = res.data.records
this.total = res.data.total
this.$message.success('删除成功!')
this.getList()
}
})
},
})
},
changeStatus (item) {
let title = item.status == '1' ? '是否要取消发布?' : '是否要发布?';
this.$confirm(title, {type: 'warning'}).then(() => {
item.status = item.status == '1' ? '0' : '1'
this.instance.post('/app/appcountrysidetourism/addOrUpdate', item).then(res => {
if (res && res.code == 0) {
title == '是否要发布?' ? this.$message.success('发布成功') : this.$message.success('取消发布成功')
this.getList()
}
})
})
},
remove(id) {
this.$confirm('确定删除该数据?').then(() => {
this.instance.post(`/app/appcountrysidetourism/delete?ids=${id}`).then(res => {
if (res.code == 0) {
this.$message.success('删除成功!')
this.getList()
}
})
})
},
toAdd(id) {
this.$emit('change', {
type: 'Add',
params: {
id: id || ''
}
})
}
toAdd(id) {
this.$emit('change', {
type: 'Add',
params: {
id: id || ''
}
})
}
}
}
</script>
<style lang="scss" scoped>
.notice {
}
.notice {
}
</style>

View File

@@ -6,11 +6,11 @@
<template slot="content">
<div class="statistics-top">
<div class="statistics-top__item">
<span>监测家庭户数</span>
<span>监测对象户数</span>
<h2 style="color: #2266FF;">{{ totalInfo['监测家庭户数'] }}</h2>
</div>
<div class="statistics-top__item">
<span>监测对象总数</span>
<span>监测对象人口总数</span>
<h2 style="color: #22AA99;">{{ totalInfo['监测对象总数'] }}</h2>
</div>
<div class="statistics-top__item">
@@ -186,7 +186,7 @@
{ prop: 'idNumber', label: '身份证号', align: 'center' },
{ prop: 'householdPhone', label: '户主联系方式', align: 'center' },
{ prop: 'address', label: '家庭住址', align: 'center' },
{ prop: 'status', label: '状态', align: 'center', formart: v => this.dict.getLabel('fpRiskPersonStatus', v) },
{ prop: 'status', label: '状态', align: 'center', formart: v => this.dict.getLabel('fpPrtpStatus', v) },
{ prop: 'girdMemberName', label: '网格员', align: 'center' },
{ prop: 'girdMemberPhone', label: '网格员电话', align: 'center' },
{ prop: 'visitCount', label: '走访次数', align: 'center' }
@@ -198,7 +198,7 @@
this.search.areaId = this.user.info.areaId
this.hideLevel = this.user.info.areaList.length - 1
this.dict.load('fpRiskPersonStatus', 'sex').then(() => {
this.dict.load('fpPrtpStatus', 'sex').then(() => {
this.getLogCount()
})
this.getTotal()

View File

@@ -95,10 +95,10 @@
<template #left>
<el-select v-model="search.joinStatus" placeholder="确认状态" size="small" clearable class="vc-input-160" @change="searchMeetinguser">
<el-option
v-for="(item,k) in confirmStatus"
:key="k"
:label="item.label"
:value="k">
v-for="(item,k) in confirmStatus"
:key="k"
:label="item.label"
:value="k">
</el-option>
</el-select>
</template>
@@ -109,10 +109,10 @@
</template>
</ai-search-bar>
<ai-table
:tableData="info.attendees"
:colConfigs="colConfigs"
style="margin-top: 12px;"
:isShowPagination="false">
:tableData="info.attendees"
:colConfigs="colConfigs"
style="margin-top: 12px;"
:isShowPagination="false">
<el-table-column slot="meetingUserName"
label="姓名"
align="center"
@@ -201,7 +201,7 @@ export default {
{
slot: 'joinStatus',
},
{ prop: 'signInStatus', align: 'center', label: '签到', formart: v => v === '1' ? '已签到' : '未签到' },
{prop: 'signInStatus', align: 'center', label: '签到', formart: v => v === '1' ? '已签到' : '未签到'},
{
slot: 'option',
}
@@ -249,11 +249,9 @@ export default {
params: {id}
}).then(res => {
if (res?.data) {
this.info = {
...res.data,
content: this.formatContent(res.data.content || ""),
files: res.data.files || []
};
let {files = [], content} = res.data
content = content.replace(/(\r\n)|(\n)/g, "<br>")
this.info = {...res.data, content, files};
this.searchMeetinguser()
}
});

View File

@@ -37,13 +37,16 @@
</el-button>
</template>
<template #right>
<ai-download url="/app/apppreventionreturntopoverty/exportAcquisitionTable" :params="{...search,ids}"
:instance="instance" fileName="导出名单" suffixName="zip">
<el-button icon="iconfont iconExported">导出名单</el-button>
</ai-download>
<ai-import :instance="instance" name="监测对象" title="导入监测对象"
suffixName="xlsx"
url="/app/apppreventionreturntopoverty/downloadTemplate"
importUrl="/app/apppreventionreturntopoverty/import"
@onSuccess="page.current=1,getTableData()"/>
<ai-download url="/app/apppreventionreturntopoverty/export" :params="{...search,ids}"
:instance="instance" fileName="监测对象导出文件"/>
<ai-download url="/app/apppreventionreturntopoverty/export" :params="{...search,ids}" :instance="instance" fileName="监测对象导出文件"/>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="page.total" :current.sync="page.current" :size.sync="page.size"
@@ -105,7 +108,7 @@ export default {
},
data() {
return {
search: {status: '',houseType: '', objectType: '', riskType: '', birthStart: '', birthEnd:'',sex: '' },
search: {status: '', houseType: '', objectType: '', riskType: '', birthStart: '', birthEnd: '', sex: '', isHousehold: 1},
page: {current: 1, size: 10, total: 0},
tableData: [],
ids: [],

View File

@@ -130,7 +130,7 @@ export default {
methods: {
getDetailInfo() {
this.data.appLeaveMessageReplyList = []
this.instance.post(`app/appleavemessage/queryDetailById?id=` + this.detailId).then((res) => {
this.instance.post(`/app/appleavemessage/queryDetailById?id=` + this.detailId).then((res) => {
this.data = res.data
this.data.images = JSON.parse(res.data.images)
if (this.data.appLeaveMessageReplyList.length) {
@@ -166,7 +166,7 @@ export default {
createUnitId: this.user.info.unitId,
createUnitName: this.user.info.unitName,
}
this.instance.post(`app/appleavemessagereply/addOrUpdate`, params).then((res) => {
this.instance.post(`/app/appleavemessagereply/addOrUpdate`, params).then((res) => {
console.log(res)
this.maskShow = false
this.getDetailInfo()
@@ -193,7 +193,7 @@ export default {
return item
})
}
this.instance.post(`app/appleavemessage/addOrUpdate`, params).then((res) => {
this.instance.post(`/app/appleavemessage/addOrUpdate`, params).then((res) => {
this.getDetailInfo()
})
})

View File

@@ -0,0 +1,79 @@
<template>
<div class="AppAnnounce">
<!-- <keep-alive :include="['List']"> -->
<component ref="component" :is="component" @change="onChange" :params="params" :instance="instance" :dict="dict"></component>
<!-- </keep-alive> -->
</div>
</template>
<script>
import List from './components/List'
import Add from './components/Add'
import Detail from './components/Detail'
export default {
name: 'AppAnnounce',
label: '群发居民群',
props: {
instance: Function,
dict: Object
},
data () {
return {
component: 'List',
params: {},
include: []
}
},
components: {
Add,
List,
Detail
},
mounted () {
if (this.$route.params.id) {
this.component = 'Detail'
this.params = {
id: this.$route.params.id
}
}
},
methods: {
onChange (data) {
if (data.type === 'Add') {
this.component = 'Add'
this.params = data.params
}
if (data.type === 'Detail') {
this.component = 'Detail'
this.params = data.params
}
if (data.type === 'list') {
this.component = 'List'
this.params = data.params
this.$nextTick(() => {
if (data.isRefresh) {
this.$refs.component.getList()
}
})
}
}
}
}
</script>
<style lang="scss">
.AppAnnounce {
height: 100%;
background: #F3F6F9;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,919 @@
<template>
<ai-detail class="AppAnnounceAdd">
<template slot="title">
<ai-title :title="id ? '编辑居民群发' : '添加居民群发'" isShowBack isShowBottomBorder @onBackClick="cancel(false)">
</ai-title>
</template>
<template slot="content">
<div class="AppAnnounceDetail-container">
<el-form ref="form" class="left" :model="form" label-width="110px" label-position="right">
<ai-card title="基本信息">
<template #content>
<div class="ai-form">
<el-form-item label="任务名称" prop="taskTitle" style="width: 100%;" :rules="[{ required: true, message: '请输入任务名称', trigger: 'blur' }]">
<el-input size="small" placeholder="请输入任务名称" v-model="form.taskTitle" :maxlength="15" show-word-limit></el-input>
</el-form-item>
<el-form-item label="发送范围" style="width: 100%;" prop="sendScope" :rules="[{ required: true, message: '请选择发送范围', trigger: 'change' }]">
<el-radio-group v-model="form.sendScope" @change="onScopeChange">
<el-radio label="0">全部居民群</el-radio>
<el-radio label="1">按部门选择</el-radio>
<el-radio label="2">按网格选择</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择群主" v-if="form.sendScope !== '0'" prop="wxGroupsName" style="width: 100%;" :rules="[{ required: true, message: '请选择选择群主', trigger: 'change' }]">
<ai-picker
:instance="instance"
multiple
:dialogTitle="form.sendScope === '2' ? '选择网格' : '选择部门'"
:ops="{label: form.sendScope === '2' ? 'girdName' : 'name'}"
:pageTitle="form.sendScope === '2' ? '网格' : '部门'"
:action="form.sendScope === '1' ? '/app/wxcp/wxdepartment/departList' : '/app/appgirdinfo/girdList'"
v-model="form.filterCriteria"
@pick="onPick"
@change="onSelcetChange">
<div class="AppAnnounceDetail-select">
<el-input size="small" class="AppAnnounceDetail-select__input" placeholder="请选择..." disabled v-model="form.wxGroupsName"></el-input>
<div class="select-left" v-if="form.wxGroups.length">
<span v-for="(item, index) in form.wxGroups" :key="index" v-if="index < 9">{{ item.groupOwnerName }}</span>
<em v-if="form.wxGroups.length > 9">{{ form.wxGroups.length }}</em>
</div>
<i v-if="!form.wxGroups.length">请选择</i>
<div class="select-right">{{ form.filterCriteria.length ? '重新选择' : '选择' }}</div>
</div>
</ai-picker>
<div class="tips">
<p>消息预计送达居民群数</p>
<span>{{ groupLen }}</span>
<el-tooltip
placement="top"
content="将由指定群主发送给TA作为群主的所有的群由于企业微信限制当超过1000个时将只发送到最近活跃的1000个群">
<i class="iconfont iconModal_Warning"></i>
</el-tooltip>
</div>
</el-form-item>
<el-form-item label="发送内容" prop="content" style="width: 100%;" :rules="[{ required: true, message: '请输入发送内容', trigger: 'blur' }]">
<el-input size="small" type="textarea" :rows="6" maxlength="1300" show-word-limit placeholder="请输入文本内容..." v-model="form.content"></el-input>
<div class="add">
<div class="fileList" v-if="fileList.length">
<div class="add-item" v-for="(item, index) in fileList" :key="index">
<div class="left">
<img :src="mapIcon(item.msgType)"/>
<span>{{ item.mpTitle || item.name || item.linkTitle }}</span>
</div>
<i @click="removeFile(index)">删除</i>
</div>
</div>
<el-popover
placement="top"
width="340"
offset="0"
trigger="hover">
<div class="add-item" slot="reference" style="width: max-content;">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/add.png"/>
<span style="color: #2266FF; font-size: 12px;">添加附件类型</span>
</div>
<div class="AppAnnounceDetail-content-wrapper">
<el-upload
ref="upload"
multiple
:file-list="fileList"
:show-file-list="false"
:before-upload="v => handleChange(v, 10, '.jpg,.png,.jpeg')"
:limit="9"
action="/app/wxcp/upload/uploadFile"
accept=".jpg,.png,.jpeg"
:on-exceed="onExceed"
:http-request="v => submitUpload(v, '1')">
<div class="content-item" trigger>
<img src="https://cdn.cunwuyun.cn/dvcp/announce/big-img.png"/>
<p>图片</p>
</div>
</el-upload>
<el-upload
ref="upload"
multiple
:file-list="fileList"
:show-file-list="false"
:before-upload="v => handleChange(v, 10, '.mp4')"
:limit="9"
action="/app/wxcp/upload/uploadFile"
accept=".mp4"
:on-exceed="onExceed"
:http-request="v => submitUpload(v, '2')">
<div class="content-item" trigger>
<img src="https://cdn.cunwuyun.cn/dvcp/announce/big-video.png"/>
<p>视频</p>
</div>
</el-upload>
<el-upload
ref="upload"
multiple
:file-list="fileList"
:show-file-list="false"
:before-upload="v => handleChange(v, 20, '.zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt')"
:limit="9"
:on-exceed="onExceed"
action="/app/wxcp/upload/uploadFile"
accept=".zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt"
:http-request="v => submitUpload(v, '3')">
<div class="content-item" trigger>
<img src="https://cdn.cunwuyun.cn/dvcp/announce/folder.png"/>
<p>文件</p>
</div>
</el-upload>
<div class="content-item" @click="isShowAddLink = true">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/site.png"/>
<p>网页</p>
</div>
<div class="content-item" @click="isShowAddMiniapp = true">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/miniapp.png"/>
<p>小程序</p>
</div>
</div>
</el-popover>
</div>
<div class="tips">
<em>从本地上传图片最大支持10MB支持JPG,PNG格式视频最大支持10MB支持MP4格式文件最大支持20MB</em>
</div>
</el-form-item>
<el-form-item label="宣发审批" prop="enableExamine" style="width: 100%;" :rules="[{ required: true, message: '请输入任务名称', trigger: 'blur' }]">
<el-switch
v-model="form.enableExamine"
active-value="1"
inactive-value="0"
active-text="开启后创建的群发任务需要审批人进行审批">
</el-switch>
</el-form-item>
<el-form-item v-if="form.enableExamine === '1'" label="审批人员" prop="examines" style="width: 100%;" :rules="[{ required: true, message: '请选择审批人员', trigger: 'change' }]">
<ai-wechat-selecter :instance="instance" v-model="form.examines" @change="onUserChange">
<div class="AppAnnounceDetail-select">
<el-input class="AppAnnounceDetail-select__input" size="small" placeholder="请选择..." v-model="form.examinesName"></el-input>
<div class="select-left" v-if="form.examines.length">
<span v-for="(item, index) in form.examines" :key="index">{{ item.name }}</span>
</div>
<i v-if="!form.examines.length">请选择</i>
<div class="select-right">{{ form.examines.length ? '重新选择' : '选择' }}</div>
</div>
</ai-wechat-selecter>
</el-form-item>
</div>
</template>
</ai-card>
</el-form>
<div class="right">
<Phone :avatar="user.info.avatar" @close="isShowPhone = false" :isShowClose="false" :content="form.content" :fileList="fileList"></Phone>
</div>
<ai-dialog
:visible.sync="isShowAddLink"
width="920px"
title="链接消息"
@close="onClose"
@onConfirm="onLinkConfirm">
<el-form ref="linkForm" :model="linkForm" label-width="110px" label-position="right">
<div class="ai-form">
<el-form-item label="标题" style="width: 100%;" prop="linkTitle" :rules="[{ required: true, message: '请输入标题', trigger: 'blur' }]">
<el-input
size="small"
placeholder="请输入标题"
maxlength="42"
show-word-limit
v-model="linkForm.linkTitle">
</el-input>
</el-form-item>
<el-form-item label="链接" style="width: 100%;" prop="linkUrl" :rules="[{ required: true, message: '请输入链接', trigger: 'blur' }]">
<el-input
size="small"
placeholder="请输入链接"
maxlength="682"
show-word-limit
v-model="linkForm.linkUrl">
</el-input>
</el-form-item>
<el-form-item label="描述" style="width: 100%;" prop="linkDesc">
<el-input
size="small"
placeholder="请输入描述"
maxlength="170"
show-word-limit
v-model="linkForm.linkDesc">
</el-input>
</el-form-item>
<el-form-item label="封面图" prop="linkPicUrl" style="width: 100%;">
<ai-uploader :instance="instance" v-model="linkForm.linkPicUrl" :limit="1"></ai-uploader>
</el-form-item>
</div>
</el-form>
</ai-dialog>
<ai-dialog
:visible.sync="isShowAddMiniapp"
width="920px"
title="小程序消息"
@close="onClose"
@onConfirm="onMiniAppForm">
<el-form ref="miniAppForm" :model="miniAppForm" label-width="130px" label-position="right">
<div class="ai-form">
<el-form-item label="小程序appid" style="width: 100%;" prop="mpAppid" :rules="[{ required: true, message: '小程序appid', trigger: 'blur' }]">
<el-input
size="small"
placeholder="小程序appid"
v-model="miniAppForm.mpAppid">
</el-input>
</el-form-item>
<el-form-item label="小程序page路径" style="width: 100%;" prop="mpPage" :rules="[{ required: true, message: '请输入小程序page路径', trigger: 'blur' }]">
<el-input
size="small"
placeholder="请输入小程序page路径"
v-model="miniAppForm.mpPage">
</el-input>
</el-form-item>
<el-form-item label="标题" style="width: 100%;" prop="mpTitle" :rules="[{ required: true, message: '请输入标题', trigger: 'blur' }]">
<el-input
size="small"
placeholder="请输入标题"
maxlength="20"
show-word-limit
v-model="miniAppForm.mpTitle">
</el-input>
</el-form-item>
<el-form-item label="封面图" prop="media" style="width: 100%;" :rules="[{ required: true, message: '请上传封面图', trigger: 'change' }]">
<ai-uploader url="/app/wxcp/upload/uploadFile?type=image" :instance="instance" isWechat v-model="miniAppForm.media" :limit="1"></ai-uploader>
</el-form-item>
</div>
</el-form>
</ai-dialog>
<ai-dialog
:visible.sync="isShowDate"
width="590px"
title="定时发送"
customFooter>
<el-form ref="dateForm" :model="dateForm" label-width="130px" label-position="right">
<div class="ai-form">
<el-form-item label="定时发送时间" style="width: 100%;" prop="choiceTime" :rules="[{ required: true, message: '请选择定时发送时间', trigger: 'change' }]">
<el-date-picker
style="width: 100%;"
v-model="dateForm.choiceTime"
type="datetime"
size="small"
:picker-options="pickerOptions"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择定时发送时间">
</el-date-picker>
</el-form-item>
</div>
</el-form>
<div class="dialog-footer" slot="footer">
<el-button @click="onClose">取消</el-button>
<el-button @click="onDateForm" type="primary" :loading="isLoading2" style="width: 92px;">确认</el-button>
</div>
</ai-dialog>
</div>
</template>
<template #footer>
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="confirm(0)" :loading="isLoading1" style="width: 120px;">通知成员发送</el-button>
<el-button type="primary" @click="confirm(1)">定时发送</el-button>
</template>
</ai-detail>
</template>
<script>
import Phone from './Phone'
import {mapActions, mapState} from 'vuex'
export default {
name: 'Add',
props: {
instance: Function,
dict: Object,
params: Object
},
components: {
Phone
},
data() {
return {
info: {},
department: [],
isLoading1: false,
isLoading2: false,
fileList: [],
isShowAddLink: false,
isShowAddMiniapp: false,
isShowDate: false,
isLoading: false,
linkForm: {
linkPicUrl: [],
linkDesc: '',
linkTitle: '',
linkUrl: ''
},
dateForm: {
choiceTime: ''
},
miniAppForm: {
mpAppid: '',
mpPage: '',
mpTitle: '',
media: []
},
form: {
content: '',
choiceTime: '',
contents: [],
enableExamine: '0',
examines: [],
wxGroups: [],
wxGroupsName: '',
sendScope: '0',
sendType: 0,
name: '',
filterCriteria: [],
taskTitle: '',
examinesName: ''
},
girdNames: '',
id: '',
tagsList: [],
pickerOptions: {
disabledDate: e => {
return e.getTime() < (Date.now() - 60 * 1000 * 60 * 24)
}
}
}
},
computed: {
...mapState(['user']),
groupLen() {
let i = 0
this.form.wxGroups.forEach(v => {
i = i + v.groupIds.split(',').length
})
return i
}
},
created() {
if (this.params && this.params.id) {
this.id = this.params.id
this.getInfo(this.params.id)
} else {
this.getWxGroups()
}
},
methods: {
...mapActions(['initOpenData', 'transCanvas']),
getInfo(id) {
this.instance.post(`/app/appmasssendingtask/queryDetailById?id=${id}`).then(res => {
if (res.code === 0) {
this.form = {
...this.form,
...res.data,
wxGroupsName: '1',
filterCriteria: res.data.filterCriteria.split(',')
}
if (res.data.girdNames) {
this.girdNames = res.data.girdNames.split(',')
}
this.dateForm.choiceTime = ''
if (res.data.examines && res.data.examines.length) {
this.form.examines = res.data.examines.map(v => {
return {
...v,
wxOpenUserId: v.examineUserId,
id: v.examineUserId,
name: v.examineUserName
}
})
this.form.examinesName = '1'
}
const content = res.data.contents.filter(v => v.msgType === '0')
if (content.length) {
this.$set(this.form, 'content', content[0].content)
}
this.fileList = res.data.contents.filter(v => v.msgType !== '0').map(v => {
return {
...v,
...v.sysFile
}
})
}
})
},
onUserChange(e) {
if (e.length) {
this.form.examinesName = '1'
} else {
this.form.wxGroupsName = ''
}
},
onScopeChange(e) {
this.form.filterCriteria = []
this.form.wxGroups = []
this.girdNames = ''
if (e === '0') {
this.getWxGroups()
} else {
this.form.filterCriteria = []
}
},
onPick(e) {
if (this.form.sendScope === '2' && e.length) {
this.girdNames = e.map(v => v.girdName)
}
},
onSelcetChange(e) {
if (e.length) {
this.form.wxGroupsName = '1'
this.$nextTick(() => {
this.getWxGroups()
})
} else {
this.form.wxGroupsName = ''
this.form.wxGroups = []
}
},
getWxGroups() {
this.instance.post(`/app/appmasssendingtask/queryWxGroups?sendScope=${this.form.sendScope}`, null, {
data: {
filterCriteria: this.form.filterCriteria.join(',')
},
headers: {'Content-Type': 'application/json;charset=utf-8'},
transformRequest: [function (data) {
return data.filterCriteria
}]
}).then(res => {
if (res.code === 0) {
this.form.wxGroups = res.data
}
})
},
onLinkConfirm() {
this.$refs.linkForm.validate((valid) => {
if (valid) {
this.fileList.push({
...this.linkForm,
linkPicUrl: this.linkForm.linkPicUrl.length ? this.linkForm.linkPicUrl[0].url : '',
msgType: '4'
})
this.isShowAddLink = false
}
})
},
onMiniAppForm() {
this.$refs.miniAppForm.validate((valid) => {
if (valid) {
this.fileList.push({
...this.miniAppForm,
msgType: '5',
...this.miniAppForm.media[0],
mediaId: this.miniAppForm.media[0].media.mediaId,
sysFileId: this.miniAppForm.media[0].id
})
this.isShowAddMiniapp = false
}
})
},
onClose() {
this.linkForm.linkPicUrl = []
this.linkForm.linkDesc = ''
this.linkForm.linkTitle = ''
this.linkForm.linkUrl = ''
this.miniAppForm.mpAppid = ''
this.miniAppForm.mpPage = ''
this.miniAppForm.mpTitle = ''
this.dateForm.choiceTime = ''
this.isShowDate = false
},
removeFile(index) {
this.fileList.splice(index, 1)
},
mapIcon(type) {
return {
1: 'https://cdn.cunwuyun.cn/dvcp/announce/img.png',
2: 'https://cdn.cunwuyun.cn/dvcp/announce/video.png',
3: 'https://cdn.cunwuyun.cn/dvcp/announce/folder.png',
4: 'https://cdn.cunwuyun.cn/dvcp/announce/site.png',
5: 'https://cdn.cunwuyun.cn/dvcp/announce/miniapp.png'
}[type]
},
onBeforeUpload(event) {
return this.onOverSize(event)
},
getExtension(name) {
return name.substring(name.lastIndexOf('.'))
},
handleChange(e, size, accept) {
const isLt10M = e.size / 1024 / 1024 < size
const suffixName = this.getExtension(e.name)
const suffixNameList = accept.split(',')
if (suffixNameList.indexOf(`${suffixName.toLowerCase()}`) === -1) {
this.$message.error(`不支持该格式`)
return false
}
if (!isLt10M) {
this.$message.error(`大小不超过${10}MB!`)
return false
}
return true
},
onExceed() {
this.$message.error(`最多上传9个附件`)
},
submitUpload(file, type) {
const fileType = {
'1': 'image',
'2': 'video',
'3': 'file'
}[type]
let formData = new FormData()
formData.append('file', file.file)
formData.append('type', fileType)
let loading = this.$loading()
this.instance.post(`/app/wxcp/upload/uploadFile`, formData, {
withCredentials: false
}).then(res => {
if (res.code == 0) {
this.fileList.push({
...res.data.file,
media: res.data.media,
msgType: type,
sysFileId: res.data.file.id,
imgPicUrl: res.data.file.url,
mediaId: res.data.media.mediaId
})
this.$message.success('上传成功')
}
}).finally(() => loading.close())
},
onDateForm() {
this.$refs.dateForm.validate((valid) => {
if (valid) {
if (new Date(this.dateForm.choiceTime).getTime() < Date.now()) {
return this.$message.error('定时发送时间不得早于当前时间')
} else {
this.confirm(1)
}
}
})
},
confirm(sendType) {
this.$refs.form.validate((valid) => {
if (valid) {
if (!this.form.wxGroups.length) {
return this.$message.error('居民群数量不能为0')
}
if (sendType === 1 && !this.dateForm.choiceTime) {
this.isShowDate = true
return false
}
const contents = [
{
content: this.form.content,
msgType: '0'
},
...this.fileList
]
if (sendType === 0) {
this.isLoading1 = true
} else {
this.isLoading2 = true
}
this.instance.post(`/app/appmasssendingtask/addOrUpdate`, {
...this.form,
id: this.params.id,
wxGroups: this.form.wxGroups,
contents,
sendType,
choiceTime: this.dateForm.choiceTime,
filterCriteria: this.form.filterCriteria.join(','),
examines: this.form.examines.length ? this.form.examines.map(v => {
return {
...v,
examineUserId: v.id,
examineUserName: v.name
}
}) : []
}).then(res => {
if (res.code == 0) {
this.$message.success('提交成功')
setTimeout(() => {
this.cancel(true)
}, 600)
} else {
this.isLoading1 = false
this.isLoading2 = false
}
}).catch(() => {
this.isLoading1 = false
this.isLoading2 = false
})
}
})
},
cancel(isRefresh) {
this.$emit('change', {
type: 'list',
isRefresh: !!isRefresh
})
}
}
}
</script>
<style lang="scss">
.el-tooltip__popper.is-dark {
max-width: 240px;
}
.AppAnnounceDetail-content-wrapper {
display: flex;
align-items: center;
.content-item {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 64px;
height: 64px;
line-height: 1;
margin-right: 4px;
text-align: center;
background: #F9F9F9;
border-radius: 2px;
cursor: pointer;
&:hover {
opacity: 0.6;
}
&:last-child {
margin-right: 0;
}
img {
width: 32px;
height: 32px;
margin-bottom: 4px;
}
p {
color: #222;
font-size: 12px;
}
}
}
.AppAnnounceAdd {
.ai-detail__content {
.ai-detail__content--wrapper {
position: relative;
max-width: 100%;
margin: 0;
height: 100%;
overflow: hidden;
}
}
.ai-form {
textarea {
border-radius: 4px 4px 0 0!important;
}
}
* {
box-sizing: border-box;
}
.add {
display: flex;
flex-direction: column;
padding: 14px 16px;
background: #F9F9F9;
border-radius: 0px 0px 4px 4px;
border: 1px solid #D0D4DC;
border-top: none;
.add-item {
display: flex;
align-items: center;
line-height: 1;
cursor: pointer;
&:hover {
opacity: 0.6;
}
img {
width: 20px;
height: 20px;
margin-right: 2px;
}
span {
color: #222;
font-size: 14px;
}
}
.fileList {
margin-bottom: 12px;
.add-item {
justify-content: space-between;
margin-bottom: 8px;
.left {
display: flex;
align-items: center;
}
i {
font-size: 14px;
cursor: pointer;
font-style: normal;
color: red;
&:hover {
opacity: 0.6;
}
}
&:last-child {
margin-bottom: 0;
}
}
}
}
.AppAnnounceDetail-container {
display: flex;
position: relative;
height: 100%;
padding: 0 20px;
overflow: auto;
overflow: overlay;
.left {
flex: 1;
margin-right: 20px;
}
.right {
position: sticky;
top: 0;
}
.AppAnnounceDetail-select {
display: flex;
align-items: center;
min-height: 32px;
line-height: 1;
background: #F5F5F5;
border-radius: 4px;
border: 1px solid #D0D4DC;
cursor: pointer;
overflow: hidden;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #26f;
}
& > i {
flex: 1;
height: 100%;
line-height: 32px;
padding: 0 12px;
color: #888888;
font-size: 14px;
font-style: normal;
border-right: 1px solid #D0D4DC;
background: #fff;
}
.AppAnnounceDetail-select__input {
position: absolute;
left: 0;
top: 0;
z-index: -1;
opacity: 0;
height: 100%;
}
.select-right {
height: 100%;
padding: 0 12px;
color: #222222;
font-size: 12px;
cursor: pointer;
transition: all ease 0.3s;
&:hover {
opacity: 0.5;
}
}
.select-left {
display: flex;
flex-wrap: wrap;
flex: 1;
padding: 5px 0 0px 12px;
border-right: 1px solid #D0D4DC;
border-radius: 4px 0 0 4px;
background: #fff;
em {
height: 22px;
line-height: 22px;
margin: 0 4px 5px 0;
color: #222222;
font-size: 12px;
font-style: normal;
}
span {
height: 22px;
line-height: 22px;
margin: 0 4px 5px 0;
padding: 0 8px;
font-size: 12px;
color: #222222;
background: #F3F4F7;
border-radius: 2px;
border: 1px solid #D0D4DC;
}
}
}
}
.tips {
display: flex;
align-items: center;
font-size: 14px;
color: #222222;
span {
margin: 0 3px;
color: #2266FF;
}
i {
color: #8899bb;
}
em {
line-height: 20px;
margin-top: 8px;
color: #888888;
font-size: 12px;
font-style: normal;
}
}
}
</style>

View File

@@ -0,0 +1,773 @@
<template>
<ai-detail class="AppAnnounceDetail">
<template slot="title">
<ai-title title="群发详情" isShowBack isShowBottomBorder @onBackClick="cancel(false)">
</ai-title>
</template>
<template slot="content">
<ai-card title="基础信息">
<template #right>
<div class="right-tips" v-if="info.status === '4'">
<el-tooltip
placement="top"
content="任务开始后3天内15分钟更新1次3天后访问页面时触发更新1小时最多刷新1次">
<i class="iconfont iconDetails"></i>
</el-tooltip>
<span>数据更新于{{ info.dataUpdateTime }}</span>
</div>
</template>
<template #content>
<ai-wrapper>
<ai-info-item label="任务名称" isLine :value="info.taskTitle"></ai-info-item>
<ai-info-item label="任务状态" isLine>
<span :style="{ color: dict.getColor('mstStatus', info.status) }">{{ dict.getLabel('mstStatus', info.status) }}</span>
</ai-info-item>
<ai-info-item label="创建人" isLine>
<div class="user">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/user.png" />
<span>{{ info.createUserName }}</span>
<span>{{ info.createUserDeptName }}</span>
</div>
</ai-info-item>
<ai-info-item label="审批人" isLine v-if="info.enableExamine === '1'">
<div class="user-wrapper">
<div class="user" v-for="(item, index) in info.examines" :key="index">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/user.png" />
<span>{{ item.examineUserName }}</span>
</div>
</div>
</ai-info-item>
<ai-info-item label="创建时间" :value="info.createTime"></ai-info-item>
<ai-info-item label="群发时间" :value="info.choiceTime"></ai-info-item>
<ai-info-item label="群发范围" isLine>
<div class="text">
<span>{{ info.sendScope === '0' ? '全部' : '按条件筛选的' }}</span>
<i>{{ groups.length }}</i>
<span>个居民群</span>
<em @click="isShowGroups = true">详情</em>
</div>
</ai-info-item>
<ai-info-item label="消息内容" isLine>
<div class="msg">
<p>{{ content }}</p>
<div class="msg-bottom">
<div class="left" v-if="fileList.length">
<img :src="mapIcon(fileList[0].msgType)" />
<span>{{ mapType(fileList[0].msgType) }}{{ fileList[0].mpTitle || fileList[0].name || fileList[0].linkTitle }} </span>
<i>{{ fileList.length }}</i>
<span>个附件</span>
</div>
<div class="left" v-else>
<span>暂无附件</span>
</div>
<div class="right" @click="isShowPhone = true">预览消息</div>
</div>
</div>
</ai-info-item>
</ai-wrapper>
</template>
</ai-card>
<ai-card>
<template #title>
<div class="AppAnnounceDetail-title">
<span :class="[currIndex === 0 ? 'active' : '']" @click="currIndex = 0">成员统计</span>
<span :class="[currIndex === 1 ? 'active' : '']" @click="currIndex = 1">居民群统计</span>
</div>
</template>
<template #content>
<div class="content-item" v-if="currIndex === 0">
<div class="top">
<div class="top-item">
<div class="top-item__title">
<h3>计划执行成员</h3>
</div>
<p>{{ memberInfo.planCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>未执行成员</h3>
</div>
<p>{{ memberInfo.unExecutedCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>已执行成员</h3>
</div>
<p>{{ memberInfo.executedCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>无法执行成员</h3>
<el-tooltip
placement="top"
content="由于员工不在可见范围、离职、客户群接收已达到上限等原因,无法执行群发任务的成员总数">
<i class="iconfont iconDetails"></i>
</el-tooltip>
</div>
<p>{{ memberInfo.cannotExecuteCount || 0 }}</p>
</div>
</div>
<div class="bottom">
<div class="bottom-search">
<div class="left">
<el-radio-group v-model="search1.sendStatus" size="small" @change="search1.current = 1, getMemberInfo()">
<el-radio-button size="small" label="0">未执行</el-radio-button>
<el-radio-button size="small" label="1">已执行</el-radio-button>
<el-radio-button size="small" label="2">无法执行</el-radio-button>
</el-radio-group>
<ai-picker
dialogTitle="选择部门"
action="/app/wxcp/wxdepartment/departList"
:instance="instance"
@pick="e => onUserChange(e, 'search1')" :multiple="false" v-model="user1">
<div class="userSelcet">
<span style="color: #606266;" v-if="search1.deptartId">{{ name1 }}</span>
<span v-else>部门</span>
<i class="el-icon-arrow-up" v-if="!search1.deptartId"></i>
<i class="el-icon-circle-close" v-if="search1.deptartId" @click.stop="user1 = [], search1.deptartId = '', search1.current = 1, getMemberInfo()"></i>
</div>
</ai-picker>
</div>
<el-button :type="isDisabled ? '' : 'primary'" :disabled="isDisabled" @click="sendMsg(0)" v-if="info.status === '4'">{{ isDisabled ? min + '分钟后可再次提醒' : '提醒成员发送' }}</el-button>
</div>
<ai-table
:tableData="tableData1"
:col-configs="colConfigs1"
:total="total1"
border
tableSize="small"
:current.sync="search1.current"
:size.sync="search1.size"
@getList="getMemberInfo">
<el-table-column slot="user" label="成员" align="left">
<template slot-scope="{ row }">
<div class="userinfo">
<span>{{ row.groupOwnerName }}</span>
<span style="color: #999">{{ row.mainDepartmentName }}</span>
</div>
</template>
</el-table-column>
</ai-table>
</div>
</div>
<div class="content-item" v-if="currIndex === 1">
<div class="top">
<div class="top-item">
<div class="top-item__title">
<h3>计划送达居民群</h3>
</div>
<p>{{ groupInfo.planCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>未送达居民群</h3>
</div>
<p>{{ groupInfo.unExecutedCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>已送达居民群</h3>
</div>
<p>{{ groupInfo.executedCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>无法送达居民群</h3>
</div>
<p>{{ groupInfo.cannotExecuteCount || 0 }}</p>
</div>
</div>
<div class="bottom">
<div class="bottom-search">
<div class="left">
<el-radio-group v-model="search2.sendStatus" size="small" @change="search2.current = 1, getGroupInfo()">
<el-radio-button size="small" label="0">未送达</el-radio-button>
<el-radio-button size="small" label="1">已送达</el-radio-button>
<el-radio-button size="small" label="2">无法送达</el-radio-button>
</el-radio-group>
<ai-picker
dialogTitle="选择部门"
action="/app/wxcp/wxdepartment/departList"
:instance="instance"
@pick="e => onUserChange(e, 'search2')" :multiple="false" v-model="user2">
<div class="userSelcet">
<span style="color: #606266;" v-if="search2.deptartId">{{ name2 }}</span>
<span v-else>部门</span>
<i class="el-icon-arrow-up" v-if="!search2.deptartId"></i>
<i class="el-icon-circle-close" v-if="search2.deptartId" @click.stop="user1 = [], search2.deptartId = '', search2.current = 1, getGroupInfo()"></i>
</div>
</ai-picker>
</div>
<el-button :type="isDisabled ? '' : 'primary'" :disabled="isDisabled" @click="sendMsg(1)" v-if="info.status === '4'">{{ isDisabled ? min + '分钟后可再次提醒' : '提醒成员发送' }}</el-button>
</div>
<ai-table
:tableData="tableData2"
:col-configs="colConfigs2"
:total="total2"
border
tableSize="small"
:current.sync="search2.current"
:size.sync="search2.size"
@getList="getGroupInfo">
<el-table-column slot="user" label="群主" align="center">
<template slot-scope="{ row }">
<div class="userinfo">
<span>{{ row.groupOwnerName }}</span>
<span style="color: #999">{{ row.mainDepartmentName }}</span>
</div>
</template>
</el-table-column>
</ai-table>
</div>
</div>
</template>
</ai-card>
<ai-dialog
:visible.sync="isShowGroups"
width="890px"
title="群发范围"
@onConfirm="isShowGroups = false">
<ai-table
:tableData="info.wxGroups"
:col-configs="colConfigs3"
border
tableSize="small"
:isShowPagination="false"
@getList="() => {}">
</ai-table>
</ai-dialog>
<div class="detail-phone" v-if="isShowPhone">
<div class="mask"></div>
<Phone :avatar="user.info.avatar" @close="isShowPhone = false" :isShowClose="true" :content="content" :fileList="fileList"></Phone>
</div>
</template>
</ai-detail>
</template>
<script>
import { mapState } from 'vuex'
import Phone from './Phone'
export default {
name: 'Detail',
props: {
instance: Function,
dict: Object,
params: Object
},
components: {
Phone
},
data () {
return {
total1: 0,
isShowGroups: false,
isShowPhone: false,
total2: 0,
user1: [],
user2: [],
name1: '',
name2: '',
radio1: '未执行',
search1: {
current: 1,
size: 10,
deptartId: '',
type: 0,
sendStatus: '0'
},
search2: {
current: 1,
size: 10,
deptartId: '',
type: 1,
sendStatus: '0'
},
memberInfo: {},
groupInfo: {},
tableData1: [],
fileList: [],
tableData2: [],
info: {},
content: '',
currIndex: 0,
colConfigs3: [
{ prop: 'groupOwnerName', label: '群主' },
{ prop: 'groupNames', label: '群名称' }
],
colConfigs1: [
{ slot: 'user', label: '成员' },
{ prop: 'groupCount', label: '预计送达居民群', align: 'center' }
],
colConfigs2: [
{ prop: 'groupName', label: '居民群' },
{ prop: 'memberCount', label: '群人数', align: 'center' },
{ slot: 'user', label: '群主', align: 'center' },
],
groups: [],
timer: null,
min: 60,
isDisabled: false,
rejecterId: ''
}
},
computed: {
...mapState(['user'])
},
created () {
this.getInfo(this.params.id)
this.getMemberInfo()
this.getGroupInfo()
},
destroyed () {
clearInterval(this.timer)
},
methods: {
getMemberInfo () {
this.instance.post(`/app/appmasssendingtask/detailStatistics`, null, {
params: {
...this.search1,
taskId: this.params.id
}
}).then(res => {
if (res.code === 0) {
this.tableData1 = res.data.executedList.records
this.total1 = res.data.executedList.total
this.memberInfo = res.data
}
})
},
onUserChange (e, search) {
if (e.length) {
search === 'search1' ? this.name1 = e[0].name : this.name2 = e[0].name
this[search].deptartId = e[0].id
} else {
this[search].deptartId = ''
search === 'search1' ? this.name1 = '' : this.name2 = ''
}
this[search].current = 1
if (search === 'search1') {
this.getMemberInfo()
} else {
this.getGroupInfo()
}
},
sendMsg () {
this.instance.post(`/app/appmasssendingtask/remindSend?id=${this.params.id}`).then(res => {
if (res.code === 0) {
this.$message.success('提醒成功')
this.getInfo(this.params.id)
}
})
},
getGroupInfo () {
this.instance.post(`/app/appmasssendingtask/detailStatistics`, null, {
params: {
...this.search2,
taskId: this.params.id
}
}).then(res => {
if (res.code === 0) {
this.tableData2 = res.data.executedList.records.map(v => {
return {
...v,
groupName: v.groupName || '未命名群聊'
}
})
this.total2 = res.data.executedList.total
this.groupInfo = res.data
}
})
},
countdown () {
this.timer = setInterval(() => {
const nowTime = this.$moment(new Date())
const min = nowTime.diff(this.info.remindTime, 'minute')
this.min = (60 - min)
if (this.min <= 0) {
this.isDisabled = false
clearInterval(this.timer)
} else {
this.isDisabled = true
}
}, 1000)
},
getInfo (id) {
this.instance.post(`/app/appmasssendingtask/queryDetailById?id=${id}`).then(res => {
if (res.code === 0) {
this.info = res.data
if (res.data.status === '4' && res.data.remindTime) {
this.countdown()
}
const content = res.data.contents.filter(v => v.msgType === '0')
if (content.length) {
this.content = content[0].content
}
this.fileList = res.data.contents.filter(v => v.msgType !== '0').map(v => {
return {
...v,
...v.sysFile
}
})
this.info.wxGroups = res.data.wxGroups.map(v => {
this.groups.push(...v.groupIds.split(','))
return {
...v,
groupIds: v.groupIds.split(',')
}
})
if (res.data.examines && res.data.examines.length) {
const user = res.data.examines.filter(v => v.examineStatus === '2')
if (user.length) {
this.rejecterId = user[0].examineUserId
}
}
}
})
},
mapType (type) {
return {
1: '图片',
2: '视频',
3: '文件',
4: '网站',
5: '小程序'
}[type]
},
mapIcon (type) {
return {
1: 'https://cdn.cunwuyun.cn/dvcp/announce/img.png',
2: 'https://cdn.cunwuyun.cn/dvcp/announce/video.png',
3: 'https://cdn.cunwuyun.cn/dvcp/announce/folder.png',
4: 'https://cdn.cunwuyun.cn/dvcp/announce/site.png',
5: 'https://cdn.cunwuyun.cn/dvcp/announce/miniapp.png'
}[type]
},
cancel (isRefresh) {
this.$emit('change', {
type: 'list',
isRefresh: !!isRefresh
})
}
}
}
</script>
<style scoped lang="scss">
.AppAnnounceDetail {
position: relative;
.user-wrapper {
display: flex;
flex-wrap: wrap;
}
.detail-phone {
position: fixed;
left: 0%;
top: 0%;
z-index: 11;
width: 100%;
height: 100%;
.mask {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 1;
background: rgba($color: #000000, $alpha: 0.6);
}
::v-deep .phone-container {
position: absolute;
left: 50%;
top: 50%;
z-index: 11;
transform: translate(-50%, -50%);
}
}
.userSelcet {
display: flex;
align-items: center;
justify-content: space-between;
width: 215px;
height: 32px;
line-height: 32px;
margin-left: 12px;
border-radius: 4px;
border: 1px solid #d0d4dc;
overflow: hidden;
cursor: pointer;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #26f;
}
i {
display: flex;
position: relative;
align-items: center;
justify-content: center;
width: 30px;
height: 100%;
line-height: 32px;
font-size: 14px;
text-align: center;
color: #d0d4dc;
transform: rotateZ(180deg);
}
.el-icon-circle-close:hover {
opacity: 0.6;
}
span {
flex: 1;
padding: 0 15px;
font-size: 12px;
color: $placeholderColor;
}
}
.userinfo {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1;
span:first-child {
margin-bottom: 4px;
}
}
.user {
display: flex;
align-items: center;
line-height: 1;
margin-right: 8px;
img {
width: 16px;
height: 16px;
margin-right: 2px;
}
span {
position: relative;
top: 2px;
color: #222222;
font-size: 12px;
}
}
.text {
display: flex;
align-items: center;
i {
color: #2266FF;
font-style: normal;
}
em {
margin-left: 8px;
color: #2266FF;
font-size: 12px;
font-style: normal;
cursor: pointer;
transition: all ease 0.3s;
&:hover {
opacity: 0.6;
}
}
}
.msg {
background: #F9F9F9;
border-radius: 2px;
border: 1px solid #D0D4DC;
p {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
line-height: 38px;
padding: 0px 12px;
overflow: hidden;
}
.msg-bottom {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
padding: 0 16px;
border-top: 1px solid #D0D4DC;
.left {
display: flex;
align-items: center;
img {
width: 16px;
height: 16px;
margin-right: 8px;
}
span {
color: #222222;
font-size: 14px;
}
i {
color: #2266FF;
font-size: 14px;
font-style: normal;
}
}
.right {
color: #2266FF;
font-size: 12px;
cursor: pointer;
&:hover {
opacity: 0.6;
}
}
}
}
::v-deep .AppAnnounceDetail-title {
display: flex;
align-items: center;
span {
height: 100%;
line-height: 56px;
margin-right: 32px;
color: #888888;
font-size: 16px;
font-weight: 600;
transition: all ease 0.3s;
border-bottom: 3px solid transparent;
cursor: pointer;
user-select: none;
&:hover {
color: #222;
}
&:last-child {
margin-right: 0;
}
&.active {
color: #222222;
border-bottom: 3px solid #2266FF;
}
}
}
.content-item {
.top {
display: flex;
align-items: center;
margin-bottom: 16px;
.top-item {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
height: 90px;
margin-right: 16px;
padding: 0 16px;
background: #F9F9F9;
border-radius: 2px;
&:last-child {
margin-right: 0;
}
.top-item__title {
display: flex;
align-items: center;
margin-bottom: 8px;
i {
margin-left: 4px;
color: #8899bb;
font-size: 16px;
}
}
h3 {
color: #222222;
font-size: 14px;
font-weight: 700;
}
p {
color: #2266FF;
font-size: 24px;
font-weight: 700;
}
}
}
.bottom-search {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.left {
display: flex;
align-items: center;
}
}
}
::v-deep .right-tips {
display: flex;
align-items: center;
i {
margin-right: 4px;
color: #8899bb;
font-size: 16px;
}
span {
color: #888888;
font-size: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,295 @@
<template>
<ai-list class="AppAnnounce">
<template slot="title">
<ai-title title="群发居民群" isShowBottomBorder>
<template #sub>
<span>管理员统一创建宣发任务选择要发送的居民群后通知群主发送群主确认后即可群发到居民群群主向同一个居民群每天最多可群发10条消息</span>
</template>
</ai-title>
</template>
<template slot="content">
<ai-search-bar class="search-bar">
<template #left>
<el-button size="small" type="primary" icon="iconfont iconAdd" @click="toAdd('')">创建宣发</el-button>
<ai-select
v-model="search.status"
@change="search.current = 1, getList()"
placeholder="任务状态"
:selectList="dict.getDict('mstStatus')">
</ai-select>
<el-date-picker
v-model="search.startTime"
type="date"
size="small"
value-format="yyyy-MM-dd"
@change="search.current = 1, getList()"
placeholder="选择群发开始日期">
</el-date-picker>
<el-date-picker
v-model="search.endTime"
type="date"
size="small"
value-format="yyyy-MM-dd"
@change="search.current = 1, getList()"
placeholder="选择群发结束日期">
</el-date-picker>
<ai-wechat-selecter :instance="instance" @change="onUserChange" :isMultiple="false" v-model="user">
<div class="userSelcet">
<span style="color: #606266;" v-if="search.createUserId">{{ name }}</span>
<span v-else>创建人</span>
<i class="el-icon-arrow-up" v-if="!search.createUserId"></i>
<i class="el-icon-circle-close" v-if="search.createUserId" @click.stop="user = [], search.createUserId = '', name = '', search.current = 1, getList()"></i>
</div>
</ai-wechat-selecter>
</template>
<template slot="right">
<el-input
v-model="search.taskTitle"
size="small"
v-throttle="() => { search.current = 1, getList() }"
placeholder="请输入任务名称"
clearable
@clear="search.current = 1, search.taskTitle = '', getList()"
suffix-icon="iconfont iconSearch">
</el-input>
</template>
</ai-search-bar>
<ai-table
:tableData="tableData"
:col-configs="colConfigs"
:total="total"
v-loading="loading"
style="margin-top: 6px; width: 100%;"
:current.sync="search.current"
:size.sync="search.size"
@getList="getList">
<el-table-column slot="user" width="140px" label="创建人" align="center">
<template slot-scope="{ row }">
<div class="userinfo">
<span>{{ row.createUserName }}</span>
<span style="color: #999">{{ row.createUserDeptName }}</span>
</div>
</template>
</el-table-column>
<el-table-column slot="options" width="140px" fixed="right" label="操作" align="center">
<template slot-scope="{ row }">
<div class="table-options">
<el-button type="text" @click="remindExamine(row.id)" v-if="['0'].includes(row.status)">催办</el-button>
<el-button type="text" @click="cancel(row.id)" v-if="['0'].includes(row.status)">撤回</el-button>
<el-button type="text" @click="toDetail(row.id)">详情</el-button>
<el-button type="text" @click="toAdd(row.id)" v-if="['1', '3'].includes(row.status)">编辑</el-button>
</div>
</template>
</el-table-column>
</ai-table>
</template>
</ai-list>
</template>
<script>
export default {
name: 'List',
props: {
instance: Function,
dict: Object
},
data() {
return {
search: {
current: 1,
size: 10,
status: '',
createUserId: '',
taskTitle: '',
startTime: '',
endTime: ''
},
name: '',
user: [],
tableData: [],
loading: false,
total: 0,
colConfigs: [
{ prop: 'taskTitle', label: '任务名称' },
{ prop: 'typeName', label: '群发类型', align: 'center' },
{ slot: 'user', label: '创建人', openType: 'userName', align: 'center' },
{ prop: 'choiceTime', label: '群发时间', align: 'center' },
{
prop: 'status',
align: 'center',
label: '状态',
render: (h, {row}) => {
return h('span', {
style: {
color: this.dict.getColor('mstStatus', row.status)
}
}, this.dict.getLabel('mstStatus', row.status))
}
},
{ prop: 'completionRate', label: '任务完成率', align: 'center', formart: v => v ? v === '0.0' ? '0%' : `${v}%` : '-' }
]
}
},
created () {
this.dict.load('mstStatus', 'mstSendType').then(() => {
this.getList()
})
},
methods: {
onUserChange (e) {
if (e.length) {
this.name = e[0].name
this.search.createUserId = e[0].id
} else {
this.search.createUserId = ''
this.name = ''
}
this.search.current = 1
this.getList()
},
getList() {
this.loading = true
this.instance.post(`/app/appmasssendingtask/list`, null, {
params: {
...this.search,
}
}).then(res => {
if (res.code == 0) {
this.tableData = res.data.records.map(v => {
return {
...v,
typeName: '群发居民群'
}
})
this.total = res.data.total
this.$nextTick(() => {
this.loading = false
})
} else {
this.loading = false
}
}).catch(() => {
this.loading = false
})
},
remindExamine (id) {
this.$confirm('确认再次通知任务审核人员?').then(() => {
this.instance.post(`/app/appmasssendingtask/remindExamine?id=${id}`).then(res => {
if (res.code == 0) {
this.$message.success('催办成功!')
this.getList()
}
})
})
},
cancel (id) {
this.$confirm('确认撤回该群发任务?').then(() => {
this.instance.post(`/app/appmasssendingtask/cancel?id=${id}`).then(res => {
if (res.code == 0) {
this.$message.success('撤回成功!')
this.getList()
}
})
})
},
remove(id) {
this.$confirm('确定删除该数据?').then(() => {
this.instance.post(`/app/appmasssendingtask/delete?ids=${id}`).then(res => {
if (res.code == 0) {
this.$message.success('删除成功!')
this.getList()
}
})
})
},
toAdd(id) {
this.$emit('change', {
type: 'Add',
params: {
id
}
})
},
toDetail (id) {
this.$emit('change', {
type: 'Detail',
params: {
id
}
})
}
}
}
</script>
<style lang="scss" scoped>
.AppAnnounce {
height: 100%;
.userinfo {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
line-height: 1;
span:first-child {
margin-bottom: 4px;
}
}
.userSelcet {
display: flex;
align-items: center;
justify-content: space-between;
width: 215px;
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #d0d4dc;
overflow: hidden;
cursor: pointer;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #26f;
}
i {
display: flex;
position: relative;
align-items: center;
justify-content: center;
width: 30px;
height: 100%;
line-height: 32px;
font-size: 14px;
text-align: center;
color: #d0d4dc;
transform: rotateZ(180deg);
}
.el-icon-circle-close:hover {
opacity: 0.6;
}
span {
flex: 1;
padding: 0 15px;
font-size: 12px;
color: $placeholderColor;
}
}
}
</style>

View File

@@ -0,0 +1,344 @@
<template>
<div class="phone-container">
<img class="close" @click="$emit('close')" v-if="isShowClose" src="https://cdn.cunwuyun.cn/dvcp/announce/close.png" />
<img class="phone" src="https://cdn.cunwuyun.cn/dvcp/announce/phone.png" />
<img class="phone-wrapper" src="https://cdn.cunwuyun.cn/dvcp/announce/phone-wrapper.png" />
<div class="right-content">
<div class="msg-list">
<div class="msg-item" v-if="content">
<div class="msg-item__left">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/avatar.png" />
</div>
<div class="msg-item__right">
<div class="msg-wrapper msg-text">
<p>{{ content }}</p>
</div>
</div>
</div>
<div class="msg-item" v-for="item in fileList" :key="item.id">
<div class="msg-item__left">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/avatar.png" />
</div>
<div class="msg-item__right" :class="[['1', '2'].indexOf(item.msgType) !== -1 ? 'left-border' : '']">
<div class="msg-wrapper msg-img" v-if="item.msgType === '1'">
<img :src="item.imgPicUrl" />
</div>
<div class="msg-wrapper msg-video" v-if="item.msgType === '2'">
<video controls :src="item.url"></video>
</div>
<div class="msg-wrapper msg-file" v-if="item.msgType === '3'">
<div class="msg-left">
<h2>{{ item.name }}</h2>
<p>{{ item.fileSizeStr }}</p>
</div>
<img :src="mapIcon(item.name)" />
</div>
<div class="msg-wrapper msg-link" v-if="item.msgType === '4'">
<h2>{{ item.linkTitle }}</h2>
<div class="msg-right">
<p>{{ item.linkDesc }}</p>
<img :src="item.linkPicUrl || 'https://cdn.cunwuyun.cn/dvcp/announce/html.png'" />
</div>
</div>
<div class="msg-wrapper msg-miniapp" v-if="item.msgType === '5'">
<h2>{{ item.mpTitle }}</h2>
<img :src="item.url" />
<div class="msg-bottom">
<i>小程序</i>
<img src="https://cdn.cunwuyun.cn/dvcp/announce/miniapp.png">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['fileList', 'avatar', 'content', 'isShowClose'],
watch: {
fileList (v) {
if (v.length) {
setTimeout(() => {
document.querySelector('.right-content').scrollTo(0, 999999)
}, 800)
}
}
},
methods: {
mapIcon (fileName) {
if (['.zip', '.rar'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/zip.png'
}
if (['.doc', '.docx'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/world.png'
}
if (['.xls', '.xlsx'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/xls.png'
}
if (['.txt'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/txt.png'
}
if (['.pdf'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/pdf.png'
}
if (['.ppt', '.pptx'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/ppt.png'
}
},
getExtension(name) {
return name.substring(name.lastIndexOf('.'))
}
}
}
</script>
<style lang="scss" scoped>
.phone-container {
width: 338px;
height: 675px;
padding: 80px 15px 100px 32px;
.phone {
position: absolute;
left: 13px;
top: 4px;
z-index: 1;
width: 314px;
height: 647px;
}
.close {
position: absolute;
top: 0;
right: 0;
z-index: 111;
width: 60px;
height: 60px;
cursor: pointer;
transition: all ease 0.5s;
transform: translate(100%, -50%);
&:hover {
opacity: 0.7;
}
}
.phone-wrapper {
position: absolute;
left: 0;
top: 0;
z-index: 2;
width: 338px;
height: 675px;
}
.right-content {
position: relative;
z-index: 11;
height: 100%;
overflow-y: auto;
.msg-item {
display: flex;
margin-bottom: 20px;
.msg-item__left {
width: 42px;
height: 42px;
margin-right: 16px;
border-radius: 4px;
flex-shrink: 1;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
.msg-item__right {
position: relative;
flex: 1;
&::after {
position: absolute;
top: 16px;
left: 0;
z-index: 1;
width: 0;
height: 0;
border-right: 6px solid #fff;
border-left: 6px solid transparent;
border-bottom: 6px solid transparent;
border-top: 6px solid transparent;
content: " ";
transform: translate(-100%, 0%);
}
&.left-border::after {
display: none;
}
.msg-img img {
max-width: 206px;
max-height: 200px;
}
.msg-video video {
max-width: 206px;
max-height: 200px;
}
.msg-text {
max-width: 206px;
width: max-content;
line-height: 1.3;
padding: 12px;
background: #FFFFFF;
border-radius: 5px;
word-break: break-all;
font-size: 14px;
color: #222222;
}
.msg-miniapp {
width: 206px;
padding: 0 12px;
text-align: justify;
font-size: 0;
background: #FFFFFF;
border-radius: 5px;
font-size: 14px;
color: #222222;
h2 {
line-height: 1.2;
padding: 8px 0;
border-bottom: 1px solid #eee;
color: #222222;
font-size: 14px;
}
& > img {
width: 100%;
height: 120px;
margin-bottom: 8px;
}
.msg-bottom {
display: flex;
align-items: center;
line-height: 1;
padding: 4px 0;
border-top: 1px solid #eee;
i {
margin-right: 4px;
font-size: 12px;
font-style: normal;
color: #999;
}
img {
width: 16px;
height: 16px;
border-radius: 50%;
}
}
}
.msg-file {
display: flex;
align-items: center;
width: 206px;
padding: 12px;
background: #FFFFFF;
border-radius: 5px;
.msg-left {
flex: 1;
margin-right: 18px;
h2 {
display: -webkit-box;
flex: 1;
line-height: 16px;
margin-bottom: 4px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
text-overflow: ellipsis;
overflow: hidden;
color: #222222;
font-size: 14px;
width: 120px;
}
p {
color: #888888;
font-size: 12px;
}
}
img {
width: 44px;
height: 44px;
border-radius: 2px;
}
}
.msg-link {
width: 206px;
padding: 12px;
background: #FFFFFF;
border-radius: 5px;
h2 {
margin-bottom: 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #222222;
font-size: 14px;
font-weight: normal;
}
.msg-right {
display: flex;
align-items: center;
p {
display: -webkit-box;
flex: 1;
line-height: 16px;
margin-right: 10px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
text-overflow: ellipsis;
overflow: hidden;
color: #888;
font-size: 12px;
}
img {
width: 50px;
height: 50px;
border-radius: 4px;
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,752 @@
<template>
<ai-list class="AppAnnounceStatistics">
<template slot="content">
<div class="statistics-content">
<ai-title title="宣发日历"></ai-title>
<div class="flex-content">
<div class="flex-left">
<div class="date-header">
<p>{{chooseYear}}{{chooseMonth}}</p>
<div>
<el-date-picker size="small"
v-model="searchMonth"
type="month" value-format="yyyy-MM"
placeholder="选择日期" @change="searchMonthChange">
</el-date-picker>
</div>
</div>
<el-calendar v-model="calendarDate">
<template
slot="dateCell"
slot-scope="{date, data}">
<div class="flex-date">
<span>{{Number(data.day.substring(8, 10))}}</span>
<span class="tips" v-if="data.day.substring(5, 7) == chooseMonth && dateList[Number(data.day.substring(8, 10))] && dateList[Number(data.day.substring(8, 10))].taskList.length">{{dateList[Number(data.day.substring(8, 10))].taskList.length}}</span>
</div>
</template>
</el-calendar>
</div>
<div class="flex-right">
<div class="title">{{chooseMonth}}{{chooseDay}}日宣发内容</div>
<div class="list-content" v-if="taskList.length">
<el-timeline >
<el-timeline-item v-for="(item, index) in taskList" :key="index">
<el-card>
<div class="flex-between">
<p class="item-title">{{item.taskTitle}}</p>
<span class="item-time" v-if="item.choiceTime">{{item.choiceTime.substring(10, 16)}}</span>
</div>
<div class="item-info item-created">
<span class="label">创建人</span>{{item.createUserName}}
</div>
<div class="item-info item-dept">
<span class="label">创建部门</span>{{item.createUserDeptName}}
</div>
<div class="flex-between">
<!-- <div class="item-info">群发类型<span>{{$dict.getLabel('mstSendType', item.sendType) || ''}}</span></div> -->
<div class="item-info"><span class="label">群发类型</span><span>群发居民群</span></div>
<span class="item-btn" @click="$router.push({name: '357e228ba8e64008ace90d095a7a0dd7', params: { id: item.id }})">详情</span>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<ai-empty v-if="!taskList.length" />
</div>
</div>
</div>
<div class="statistics-content">
<div class="flex-between mar-b16">
<ai-title title="宣发效果"></ai-title>
<div class="right-search">
<div class="time-select" :class="effectType == index ? 'active' : ''" v-for="(item, index) in dateTypeList" :key="index" @click="changeEffectType(index)">{{item}}</div>
<ai-picker :instance="instance" @pick="e => onUserChange(e)" :multiple="false" dialogTitle="选择部门" action="/app/wxcp/wxdepartment/departList">
<div class="time-select">
<span class="dept-name" style="color:#999;" v-if="deptList && !deptList.length">宣发部门</span>
<span class="dept-name" v-else>{{deptList[0].name}}</span>
<i class="el-icon-arrow-down"></i>
</div>
</ai-picker>
</div>
</div>
<div class="line-content">
<div class="flex1">
<div class="header">
<p>累计创建宣发任务数</p>
<h2>{{effectData.createCount}}</h2>
</div>
<div class="chart-content">
<div class="chart-title">宣发任务数</div>
<div class="chart-box" id="createChart"></div>
</div>
</div>
<div class="flex1">
<div class="header">
<p>累计执行宣发次数</p>
<h2>{{effectData.executeCount}}</h2>
</div>
<div class="chart-content">
<div class="chart-title">宣发次数</div>
<div class="chart-box" id="executeChart"></div>
</div>
</div>
<div class="flex1 mar-r0">
<div class="header">
<p>累计触达人次</p>
<h2>{{effectData.receiveCount}}</h2>
</div>
<div class="chart-content">
<div class="chart-title">触达人次</div>
<div class="chart-box" id="receiveChart"></div>
</div>
</div>
</div>
</div>
<div class="statistics-content">
<div class="flex-between mar-b16">
<ai-title title="宣发明细"></ai-title>
<div class="right-search">
<div class="time-select" :class="departType == index ? 'active' : ''" v-for="(item, index) in dateTypeList" :key="index" @click="changeDepartType(index)">{{item}}</div>
</div>
</div>
<div id="departBarChart" v-if="isDepartData"></div>
<ai-empty v-if="!isDepartData"></ai-empty>
</div>
<ai-dialog :visible.sync="dialogDate" title="选择时间" width="500px" customFooter>
<el-date-picker v-model="timeList" size="small" type="daterange" value-format="yyyy-MM-dd"
range-separator="" start-placeholder="开始日期" end-placeholder="结束日期">
</el-date-picker>
<el-button slot="footer" @click="selectDete" type="primary">确认</el-button>
</ai-dialog>
</template>
</ai-list>
</template>
<script>
import * as echarts from "echarts";
import { mapActions, mapState } from 'vuex';
export default {
name: 'AppAnnounceStatistics',
label: '协同宣发统计',
props: {
instance: Function,
dict: Object,
permissions: Function
},
data () {
return {
calendarDate: new Date(),
dateList: {},
chooseYear: '',
chooseMonth: '',
chooseDay: '',
searchMonth: '',
taskList: [],
effectType: 0, // 宣发效果类型 0近七天、1近30天、2近一年、3自定义
effectData: {},
createChart: null,
executeChart: null,
receiveChart: null,
departType: 0, // 宣发明细类型 0近七天、1近30天、2近一年、3自定义
dateTypeList: ['近7天', '近30天', '近1年', '自定义'],
departData: {},
departBarChart: null,
dialogDate: false,
timeListEffect: '',
timeListDepart: '',
timeList: '',
isEffectTimeSelect: false,
deptList: [],
selectDeptName: '',
isDepartData: true,
departBarData: [],
type: '',
}
},
computed: {
...mapState(['user']),
},
watch: {
calendarDate: function() {
var year = '' , month = '', date = ''
if(this.calendarDate.length == 9) { // 月份选择器触发
year = this.calendarDate.substring(0, 4)
month = this.calendarDate.substring(5, 7)
date = this.calendarDate.substring(8, 10)
}else { // 日历点击
year = this.calendarDate.getFullYear();
month = this.calendarDate.getMonth() + 1;
date = this.calendarDate.getDate()
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if(this.chooseMonth != month) { // 日历点击不同月
this.searchMonth = ''
}
}
this.chooseDay = date
if(this.chooseMonth != month || this.chooseYear != year) { // 不同年/不同月重新请求日历列表
this.getCalendarList(year, month)
} else {
this.getTaskList(date)
}
this.chooseMonth = month
this.chooseYear = year
}
},
created() {
var year = this.calendarDate.getFullYear();
var month = this.calendarDate.getMonth() + 1;
var date = this.calendarDate.getDate()
if (month >= 1 && month <= 9) {
month = "0" + month;
}
this.chooseMonth = month
this.chooseYear = year
this.chooseDay = date
this.getCalendarList(year, month)
this.getEffect()
this.getDepart()
this.dict.load('mstSendType')
},
methods: {
...mapActions(['initOpenData', 'transCanvas']),
onUserChange (e) {
this.deptList = e
this.getEffect()
},
selectDete() {
if(!this.timeList || !this.timeList.length) {
return this.$message.error('请选择自定义时间');
}
if(this.isEffectTimeSelect) { //宣发效果
this.timeListEffect = this.timeList
this.effectType = 3
this.getEffect()
} else { //宣发明细
this.timeListDepart = this.timeList
this.departType = 3
this.getDepart()
}
this.dialogDate = false
},
searchMonthChange() {
this.calendarDate = this.searchMonth + '-1'
},
getCalendarList(year, month){
this.instance.post(`/app/appmasssendingtask/statisticsCalendar?yyyyMM=${year}${month}`).then(res => {
if (res.code == 0) {
this.dateList = res.data
this.getTaskList(this.chooseDay)
}
})
},
getTaskList(day) {
this.taskList = this.dateList[day].taskList
},
changeEffectType(type) {
if(this.effectType != 3) {
this.timeList = []
}else {
this.timeList = this.timeListEffect
}
if(type == 3) {
this.isEffectTimeSelect = true
this.dialogDate = true
}else {
this.effectType = type
this.getEffect()
}
},
getEffect() {
var startTime = this.timeListEffect[0] || '' , endTime = this.timeListEffect[1] || '', departId = ''
if(this.deptList && this.deptList.length) {
departId = this.deptList[0].id
}
this.instance.post(`/app/appmasssendingtask/statisticsEffect?type=${this.effectType}&startTime=${startTime}&endTime=${endTime}&departId=${departId}`).then(res => {
if (res.code == 0) {
this.effectData = res.data
var xData = [], createData = [], executeData = [], receiveData = []
res.data.trend.map(e => {
if(this.effectType == 0 || this.effectType == 1) {
e.ymd = e.ymd.substring(5, 10)
}
xData.push(e.ymd)
createData.push(e.createCount)
executeData.push(e.executeCount)
receiveData.push(e.receiveCount)
})
this.setLineChart(xData, createData, 'createChart', ['#2891FF'])
this.setLineChart(xData, executeData, 'executeChart', ['#FFB865'])
this.setLineChart(xData, receiveData, 'receiveChart', ['#26D52B'])
}
})
},
setLineChart(xData, yData, id, colorList) {
this[id] = echarts.init(document.querySelector(`#${id}`))
var option = {
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
grid: {
left: '10px',
right: '28px',
bottom: '14px',
top: '30px',
containLabel: true
},
tooltip: {
trigger: 'axis'
},
legend: {
type: "plain"
},
color: colorList,
series: [
{
data: yData,
type: 'line'
}
]
}
this[id].setOption(option)
},
changeDepartType(type) {
if(this.departType != 3) {
this.timeList = []
}else {
this.timeList = this.timeListDepart
}
if(type == 3) {
this.isEffectTimeSelect = false
this.dialogDate = true
}else {
this.departType = type
this.getDepart()
}
},
getDepart() {
var startTime = this.timeListDepart[0] || '' , endTime = this.timeListDepart[1] || ''
this.instance.post(`/app/appmasssendingtask/statisticsDepart?type=${this.departType}&startTime=${startTime}&endTime=${endTime}`).then(res => {
if (res.code == 0) {
if(res.data && res.data.length) {
this.isDepartData = true
var xData = [], yData = []
res.data.map((item) => {
this.departBarData.push(item)
xData.push(item.deptName)
yData.push(item.taskCount)
})
this.setBarChart(xData, yData)
}else {
this.isDepartData = false
}
}
})
},
setBarChart(xData, yData) {
this.departBarChart = echarts.init(document.querySelector(`#departBarChart`))
var option = {
color: ['#2891FF'],
grid: {
top: '10%',
left: '2%',
right: '2%',
bottom: 90,
containLabel: true
},
// toolbox: {
// feature: {
// dataZoom: {
// yAxisIndex: false
// },
// saveAsImage: {
// pixelRatio: 2
// }
// }
// },
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (data) => {
var index = data[0].dataIndex
return `<ww-open-data type="departmentName" openid="${this.departBarData[index].deptId}"></ww-open-data><br/>宣发任务数:${data[0].value}`
}
},
dataZoom: [
{
type: 'inside'
},
{
type: 'slider'
}
],
xAxis: {
data: xData,
silent: false,
splitLine: {
show: false
},
splitArea: {
show: false
}
},
yAxis: {
splitArea: {
show: false
}
},
series: [
{
type: 'bar',
data: yData,
barWidth: 20,
barGap: '250%',
large: true
}
]
};
// {
// tooltip: {
// trigger: 'axis',
// axisPointer: {
// type: 'shadow'
// }
// },
// grid: {
// top: '10%',
// left: '2%',
// right: '2%',
// bottom: '2%',
// containLabel: true
// },
// color: ['#2891FF'],
// xAxis: {
// type: 'category',
// data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
// },
// yAxis: {
// type: 'value'
// },
// series: [
// {
// data: [120, 200, 150, 80, 70, 110, 130],
// type: 'bar',
// barWidth: 20,
// barGap: '250%',
// }
// ]
// };
this.departBarChart.setOption(option)
}
}
}
</script>
<style lang="scss" scoped>
.AppAnnounceStatistics {
height: 100%;
.flex-between{
display: flex;
justify-content: space-between;
}
.mar-b16{
margin-bottom: 16px;
}
.mar-r0{
margin-right: 0!important;
}
.statistics-content{
padding: 0 24px 24px;
background-color: #fff;
box-shadow: 0px 4px 6px -2px rgba(15,15,21,0.1500);
border-radius: 4px;
margin-bottom: 20px;
.flex-content{
width: 100%;
display: flex;
margin-top: 16px;
.flex-left{
width: 50%;
.date-header{
padding: 12px 16px;
border: 1px solid #eee;
display: flex;
justify-content: space-between;
p{
line-height: 32px;
}
}
.flex-date{
display: flex;
justify-content: space-between;
}
.tips{
display: inline-block;
padding: 0 4px;
height: 16px;
line-height: 16px;
border-radius: 8px;
background: #2891FF;
font-size: 12px;
font-family: ArialMT;
color: #FFF;
margin-top: 8px;
}
}
.flex-right{
width: 50%;
margin-left: 16px;
border: 1px solid #eee;
.title{
line-height: 56px;
border-bottom: 1px solid #EEE;
padding-left: 16px;
font-size: 16px;
font-family: MicrosoftYaHeiSemibold;
color: #333;
}
.list-content{
padding: 16px;
height: 339px;
box-sizing: border-box;
overflow-y: scroll;
background-color: #F9F9F9;
box-sizing: border-box;
.item-title{
width: calc(100% - 100px);
word-break: break-all;
margin-bottom: 8px;
font-size: 16px;
font-family: MicrosoftYaHeiSemibold;
color: #222;
line-height: 24px;
}
.item-time{
width: 100px;
text-align: right;
font-size: 16px;
font-family: ArialMT;
color: #888;
line-height: 24px;
}
.item-info{
display: inline-block;
font-size: 14px;
font-family: MicrosoftYaHei;
color: #222;
line-height: 22px;
span{
display: inline-block;
color: #222;
word-break: break-all;
// vertical-align: text-top;
}
.label{
color: #999;
}
}
.item-created{
width: 152px;
margin-bottom: 4px;
.label{
width: 56px;
}
.name{
width: calc(100% - 56px);
}
}
.item-dept{
width: calc(100% - 152px);
.label{
width: 70px;
}
.name{
width: calc(100% - 70px);
}
}
.item-btn{
color: #26f;
cursor: pointer;
}
}
}
}
.right-search{
margin-top: 10px;
div{
display: inline-block;
}
.time-select{
font-size: 14px;
font-family: MicrosoftYaHei;
color: #222;
line-height: 22px;
padding: 6px 12px;
border-radius: 2px;
border: 1px solid #D0D4DC;
margin-right: 8px;
box-sizing: border-box;
cursor: pointer;
.dept-name{
display: inline-block;
width: 200px;
height: 22px;
overflow:hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: bottom;
}
.el-icon-arrow-down{
vertical-align: middle;
}
}
.active{
border: 1px solid #26f;
color: #26f;
}
}
.line-content{
display: flex;
.flex1{
flex: 1;
margin-right: 16px;
.header{
padding: 16px;
width: 100%;
height: 90px;
background: #F9F9F9;
border-radius: 2px;
box-sizing: border-box;
margin-bottom: 16px;
p{
font-size: 14px;
font-family: MicrosoftYaHeiSemibold;
color: #222;
line-height: 22px;
margin-bottom: 4px;
}
h2{
font-size: 24px;
font-family: DINAlternate-Bold, DINAlternate;
font-weight: bold;
color: #26F;
line-height: 32px;
}
}
.chart-content{
width: 100%;
padding: 16px;
background: #F9F9F9;
border-radius: 2px;
box-sizing: border-box;
.chart-title{
font-size: 16px;
font-family: MicrosoftYaHeiSemibold;
color: #333;
line-height: 24px;
}
.chart-box{
width: 100%;
height: 280px;
}
}
}
}
#departBarChart{
width: 100%;
height: 300px;
}
}
::v-deep .el-calendar-table:not(.is-range) td.next,
::v-deep .el-calendar-table:not(.is-range) td.prev {
color: #ccc;
}
::v-deep .el-calendar-table .el-calendar-day{
height: 48px;
line-height: 32px;
padding-left: 12px;
font-size: 14px;
font-family: ArialMT;
}
.el-calendar-table:not(.is-range) td .current{
color: #888;
}
::v-deep .el-calendar__header{
display: none;
}
::v-deep .el-calendar__body{
padding: 0;
}
::v-deep .el-calendar-table thead th:nth-of-type(1){
border-left: 1px solid #eee;
}
::v-deep .el-calendar-table thead th:nth-of-type(7){
border-right: 1px solid #eee;
}
::v-deep .el-calendar-table tr td:first-child {
border-left: 1px solid #eee;
}
::v-deep .el-calendar-table tr:first-child td {
border-top: 1px solid #eee;
}
::v-deep .el-calendar-table td {
border-bottom: 1px solid #eee;
border-right: 1px solid #eee;
}
::v-deep .el-timeline-item__timestamp.is-top{
margin-bottom: 0;
padding-top: 0;
}
::v-deep .el-timeline-item__node{
background-color: #26F;
width: 8px;
height: 8px;
border-radius: 50%;
left: 1px;
}
::v-deep .el-card{
border: none;
}
::v-deep .el-card__body{
padding: 8px;
}
}
::v-deep .ai-list__content {
padding: 0!important;
.ai-list__content--right-wrapper {
background: transparent!important;
box-shadow: none!important;
margin: 0!important;
padding: 0 0 0!important;
}
}
::v-deep .AiPicker{
display: inline-block;
}
</style>

1222
project/dv/apps/AppPdDv.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,282 @@
<template>
<div class="AiGrid" ref="container">
<div
class="AiGrid-wrapper"
ref="tree"
id="tree"
:style="{left: x, top: y, transform: `scale(${scale}) translate(-50%, -50%) `, 'transform-origin': `${0} ${0}`}">
<VueOkrTree
:props="props"
node-key="id"
show-collapsable
show-node-num
current-lable-class-name="aigrid-active"
:default-expanded-keys="defaultExpandedKeys"
ref="VueOkrTree"
@node-click="onNodeClick"
:data="treeData">
</VueOkrTree>
</div>
</div>
</template>
<script>
import AiOkrTree from "dvcp-dv-ui/components/AiOkrTree/AiOkrTree"
export default {
name: 'AiGrid',
props: ['instance'],
components: {
VueOkrTree: AiOkrTree
},
data() {
return {
scale: 1,
x: '50%',
y: '50%',
defaultExpandedKeys: [],
treeData: [],
props: {
label: 'girdName',
children: 'children'
}
}
},
mounted() {
this.bindEvent()
this.getPartyOrg()
},
destroyed() {
document.querySelector('body').removeEventListener('mousewheel', this.onMousewheel)
document.querySelector('body').removeEventListener('mouseup', this.onMouseUp)
document.querySelector('body').removeEventListener('mousedown', this.onMousedown)
document.querySelector('body').removeEventListener('mousemove', this.onMouseMove)
},
methods: {
bindEvent() {
document.querySelector('body').addEventListener('mousewheel', this.onMousewheel, true)
document.querySelector('body').addEventListener('mouseup', this.onMouseUp, true)
document.querySelector('body').addEventListener('mousedown', this.onMousedown, true)
document.querySelector('body').addEventListener('mousemove', this.onMouseMove, true)
},
onMousewheel(event) {
if (!event) return false
const elClass = event.target.className
if (elClass === 'tree' || elClass === 'middle' || (elClass && (elClass.indexOf('chart') > -1 || elClass.indexOf('user') > -1))) {
var dir = event.deltaY > 0 ? 'Up' : 'Down'
if (dir === 'Up') {
this.scale = this.scale - 0.12 <= 0.1 ? 0.1 : this.scale - 0.12
} else {
this.scale = this.scale + 0.12
}
}
return false
},
onMousedown(e) {
const elClass = e.target.className
if ((elClass && (elClass.indexOf('chart') > -1 || elClass.indexOf('user') > -1))) {
const left = document.querySelector('#tree').offsetLeft
const top = document.querySelector('#tree').offsetTop
this.isMove = true
this.offsetX = e.clientX - left
this.offsetY = e.clientY - top
}
},
onMouseMove(e) {
if (!this.isMove) return
this.x = (e.clientX - this.offsetX) + 'px'
this.y = (e.clientY - this.offsetY) + 'px'
},
onMouseUp() {
this.isMove = false
},
onNodeClick(e) {
this.$emit('nodeClick', e)
},
getPartyOrg() {
this.instance.post('/app/appgirdinfo/listAll3').then(res => {
if (res.code === 0) {
this.treeData = res.data.filter(e => !e.parentGirdId)
const parentGirdId = this.treeData[0].id
this.treeData.map(p => this.addChild(p, res.data.map(v => {
if (v.id === parentGirdId) {
this.defaultExpandedKeys.push(v.id)
}
return {
...v,
girdName: v.girdName.substr(0, 11)
}
}), {
parent: 'parentGirdId'
}))
this.$nextTick(() => {
this.autoScale()
this.$refs.VueOkrTree.setCurrentKey(parentGirdId)
})
}
})
},
autoScale() {
const treeWidth = this.$refs.tree.offsetWidth
const containerWidth = this.$refs.container.offsetWidth - 100
this.scale = treeWidth < containerWidth ? 1 : containerWidth / treeWidth
this.x = '50%'
this.y = '50%'
}
}
}
</script>
<style lang="scss" scoped>
.AiGrid {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
.AiGrid-wrapper {
display: flex;
position: absolute;
align-items: center;
left: 50%;
top: 50%;
padding: 20px;
overflow: hidden;
width: max-content;
height: 300%;
}
.aigrid-active {
background: linear-gradient(180deg, #42C6CE 0%, #307598 100%);
}
::v-deep .org-chart-container {
color: #FFFFFF;
font-size: 16px;
.org-chart-node-children {
display: flex;
justify-content: center;
float: initial !important;
}
.org-chart-node-btn {
border: 1px solid #23A0AC !important;
font-size: 16px;
font-weight: bold;
background: #071030;
color: #FF9A02;
&:after, &::before {
display: none;
}
&.expanded::before {
display: block;
position: absolute;
top: 50%;
left: 4px;
right: 4px;
height: 0;
border-top: 1px solid #FF9A02;
content: "";
}
.org-chart-node-btn-text {
background: transparent;
color: #FF9A02;
}
}
.org-chart-node {
// overflow: hidden;
.org-chart-node-label {
width: 40px;
height: 254px;
margin-right: 15px;
padding: 0 0;
.org-chart-node-label-inner {
width: 40px !important;
height: 254px !important;
border: 1px solid;
background: linear-gradient(180deg, rgba(69, 210, 218, 0.2500) 0%, rgba(69, 210, 218, 0.1000) 100%) !important;
border-image: linear-gradient(180deg, rgba(5, 185, 203, 1), rgba(73, 214, 207, 1)) 1 1 !important;
line-height: 1.3;
padding: 10px 8px;
text-align: center;
font-size: 18px;
color: rgba(255, 255, 255, 0.8);
&.aigrid-active {
background: linear-gradient(180deg, #42C6CE 0%, #307598 100%) !important;
}
}
&.is-root-label {
width: auto !important;
min-width: 240px;
height: 40px !important;
line-height: 40px !important;
min-height: 40px !important;
text-align: center;
.org-chart-node-label-inner {
padding: 0 30px !important;
color: #fff !important;
width: auto !important;
min-width: 240px;
height: 40px !important;
line-height: 40px !important;
min-height: 40px !important;
text-align: center;
background: linear-gradient(180deg, rgba(69, 210, 218, 0.2500) 0%, rgba(69, 210, 218, 0.1000) 100%) !important;
border-image: linear-gradient(180deg, rgba(5, 185, 203, 1), rgba(73, 214, 207, 1)) 1 1 !important;
&.aigrid-active {
background: linear-gradient(180deg, #42C6CE 0%, #307598 100%) !important;
}
}
}
}
&:last-child {
.org-chart-node-label {
margin-right: 0;
}
}
}
.org-chart-node-children:before, .org-chart-node:after, .org-chart-node:last-child:before,
.org-chart-node.is-leaf:before {
border-radius: 0;
border-color: #23A0AC !important;
}
.vertical .org-chart-node:after, .vertical .org-chart-node:before {
border-radius: 0;
border-color: #23A0AC !important;
}
}
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<div class="DonutChart" :id="id">
<canvas :id="canvasId"></canvas>
<div class="DonutChart-text">
<span>{{ ratio || 0 }}%</span>
<i>{{ text }}</i>
</div>
</div>
</template>
<script>
export default {
props: ['ratio', 'text'],
data () {
return {
id: `DonutChart-${Math.ceil(Math.random() * 10000)}`,
canvasId: `DonutChartCanvas-${Math.ceil(Math.random() * 10000)}`,
canvasWidth: 90,
canvasHeight: 90
}
},
mounted () {
this.$nextTick(() => {
this.init()
})
},
methods: {
drawLine(ctx, options) {
const { beginX, beginY, endX, endY, lineColor, lineWidth } = options
ctx.lineWidth = lineWidth
ctx.strokeStyle = lineColor
ctx.beginPath()
ctx.moveTo(beginX, beginY)
ctx.lineTo(endX, endY)
ctx.closePath()
ctx.stroke()
},
angle (a, i, ox, oy, or) {
var hudu = (2 * Math.PI / 360) * a * i
var x = ox + Math.sin(hudu) * or
var y = oy - Math.cos(hudu) * or
return x + '_' + y
},
mapColor (value) {
if (value < 25) {
return '#FFC139'
}
if (value < 50) {
return '#21E03E'
}
return '#05C8FF'
},
init () {
const ctx = document.querySelector(`#${this.canvasId}`).getContext('2d')
const canvasWidth = document.querySelector(`#${this.id}`).offsetWidth
const canvasHeight = document.querySelector(`#${this.id}`).offsetHeight
const angle = this.ratio / 100 * 2
let radian = 0
ctx.width = canvasWidth
ctx.height = canvasHeight
const x = canvasWidth / 2
const y = canvasHeight / 2
ctx.lineWidth = 2
ctx.strokeStyle = '#383f56'
ctx.beginPath();
ctx.arc(x, y, x - 3, 0, 2 * Math.PI)
ctx.stroke()
ctx.beginPath()
ctx.lineWidth = 4
ctx.strokeStyle = 'rgba(76, 202, 227, 1)'
if (this.ratio < 25) {
radian = 3 / 2 + angle
ctx.arc(x, y, x - 4, Math.PI + Math.PI / 2, Math.PI * radian, false)
} else if (this.ratio === 100) {
ctx.arc(x, y, x - 4, 0, Math.PI * 2)
} else {
radian = (this.ratio - 25) / 100 * 2
ctx.arc(x, y, x - 4, Math.PI + Math.PI / 2, Math.PI * radian, false)
}
ctx.stroke()
}
}
}
</script>
<style lang="scss" scoped>
.DonutChart {
position: relative;
width: 84px;
height: 84px;
overflow: hidden;
.DonutChart-text {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
flex-direction: column;
top: 50%;
left: 50%;
z-index: 1;
width: 100%;
height: 100%;
line-height: 1;
transform: translate(-50%, -50%);
span {
margin-bottom: 8px;
font-size: 20px;
font-weight: bold;
color: #fff;
font-style: oblique;
}
i {
font-size: 12px;
font-style: normal;
color: rgba(42, 183, 209, 1);
}
}
}
</style>

View File

@@ -0,0 +1,483 @@
<template>
<div class="pdgrid">
<div class="pdgrid-title">
<h2>{{ currGird }}</h2>
</div>
<div class="pdgrid-grid__title" @click="isShowGrid2 = true">
<h2 :title="girdName2">{{ girdName2 }}</h2>
</div>
<div class="pdgrid-body">
<div class="pdgrid-body__item" @click="isShowGrid3 = true">
<h2>{{ girdNum3 }}</h2>
<div class="bottom">
<i></i>
<p>{{ girdName3 }}</p>
<i class="right"></i>
</div>
</div>
<div class="pdgrid-body__item" @click.stop="isShowGrid4 = true">
<h2>{{ girdNum4 }}</h2>
<div class="bottom">
<i></i>
<p>{{ girdName4 }}</p>
<i class="right"></i>
</div>
</div>
<div class="pdgrid-body__item" @click="isShowGrid5 = true">
<h2>{{ girdNum5 }}</h2>
<div class="bottom">
<i></i>
<p>{{ girdName5 }}</p>
<i class="right"></i>
</div>
</div>
</div>
<transition name="fade">
<div class="grid-dialog" v-show="isShowGrid2">
<div class="mask" @click="isShowGrid2 = false"></div>
<div class="grid-container">
<h2 :title="girdName2">{{ girdName2 }}</h2>
<div class="grid-list">
<div
:class="[currIndex2 === index ? 'grid-active' : '']"
v-for="(item, index) in girdInfoList2"
:key="index"
:title="item.girdName"
@click.stop="onGrid2Click(item, index)">
{{ item.girdName }}
</div>
</div>
</div>
</div>
</transition>
<transition name="fade">
<div class="grid-dialog" v-show="isShowGrid3">
<div class="mask" @click="isShowGrid3 = false"></div>
<div class="grid-container">
<h2 :title="girdName3">{{ girdName3 }}</h2>
<div class="grid-list">
<div
:class="[currIndex3 === index ? 'grid-active' : '']"
v-for="(item, index) in girdInfoList3"
:key="index"
:title="item.girdName"
@click.stop="onGrid3Click(item, index)">
{{ item.girdName }}
</div>
</div>
</div>
</div>
</transition>
<transition name="fade">
<div class="grid-dialog" v-show="isShowGrid4">
<div class="mask" @click="isShowGrid4 = false"></div>
<div class="grid-container">
<h2 :title="girdName4">{{ girdName4 }}</h2>
<div class="grid-list">
<div
:class="[currIndex4 === index ? 'grid-active' : '']"
v-for="(item, index) in girdInfoList4"
:key="index"
:title="item.girdName"
@click.stop="onGrid4Click(item, index)">
{{ item.girdName }}
</div>
</div>
</div>
</div>
</transition>
<transition name="fade">
<div class="grid-dialog" v-show="isShowGrid5">
<div class="mask" @click="isShowGrid5 = false"></div>
<div class="grid-container">
<h2 :title="girdName5">{{ girdName5 }}</h2>
<div class="grid-list">
<div
:class="[currIndex5 === index ? 'grid-active' : '']"
v-for="(item, index) in girdInfoList5"
:key="index"
:title="item.girdName"
@click.stop="onGrid5Click(item, index)">
{{ item.girdName }}
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
name: 'pdgrid',
props: ['instance'],
data () {
return {
isShowGrid2: false,
isShowGrid3: false,
isShowGrid4: false,
isShowGrid5: false,
currIndex2: 0,
currIndex3: 0,
currIndex4: 0,
currIndex5: 0,
girdInfoList2: [],
girdInfoList3: [],
girdInfoList4: [],
girdInfoList5: [],
girdName2: '',
girdName3: '',
girdName4: '',
girdName5: '',
girdNum3: 0,
girdNum4: 0,
girdNum5: 0,
currGird: ''
}
},
mounted () {
this.$nextTick(() => {
document.addEventListener('keydown', this.onKeyDown)
})
this.getInfo()
},
destroyed () {
document.removeEventListener('keydown', this.onKeyDown)
},
methods: {
onKeyDown (e) {
if (e.keyCode == 27) {
this.isShowGrid2 = false
this.isShowGrid3 = false
this.isShowGrid4 = false
this.isShowGrid5 = false
}
},
onGrid2Click (item, index) {
this.currIndex2 = index
this.girdName2 = item.girdName
this.currIndex3 = -1
this.currIndex4 = -1
this.currIndex5 = -1
this.isShowGrid2 = false
this.girdInfoList3 = []
this.girdInfoList4 = []
this.girdInfoList5 = []
this.$emit('nodeClick', item.id)
this.currGird = item.girdName
this.getInfo(item.id)
},
onGrid3Click (item, index) {
this.currIndex3 = index
this.girdName3 = item.girdName
this.currIndex4 = -1
this.currIndex5 = -1
this.girdNum3 = 1
this.isShowGrid3 = false
this.$emit('nodeClick', item.id)
this.girdInfoList4 = []
this.girdInfoList5 = []
this.currGird = item.girdName
this.getInfo(item.id)
},
onGrid4Click (item, index) {
this.currIndex4 = index
this.girdName4 = item.girdName
this.currIndex5 = -1
this.girdNum4 = 1
this.isShowGrid4 = false
this.$emit('nodeClick', item.id)
this.girdInfoList5 = []
this.currGird = item.girdName
this.getInfo(item.id)
},
onGrid5Click (item, index) {
this.currIndex5 = index
this.girdName5 = item.girdName
this.isShowGrid5 = false
this.girdNum5 = 1
this.$emit('nodeClick', item.id)
this.currGird = item.girdName
this.getInfo(item.id)
},
getInfo (id) {
this.instance.post(`/app/appgirdinfo/queryPdDetailByGirdId?id=${id || ''}`).then(res => {
if (res.code === 0) {
res.data.girdInfoList2 && (this.girdInfoList2 = res.data.girdInfoList2)
res.data.girdInfoList3 && (this.girdInfoList3 = res.data.girdInfoList3)
res.data.girdInfoList4 && (this.girdInfoList4 = res.data.girdInfoList4)
res.data.girdInfoList5 && (this.girdInfoList5 = res.data.girdInfoList5)
res.data.girdName2 && (this.girdName2 = res.data.girdName2)
res.data.girdName3 && (this.girdName3 = res.data.girdName3)
res.data.girdName4 && (this.girdName4 = res.data.girdName4)
res.data.girdName5 && (this.girdName5 = res.data.girdName5)
res.data.girdNum3 != null && (this.girdNum3 = res.data.girdNum3)
res.data.girdNum4 != null && (this.girdNum4 = res.data.girdNum4)
res.data.girdNum5 != null && (this.girdNum5 = res.data.girdNum5)
if (!id) {
this.currGird = res.data.girdName2
this.currIndex2 = res.data.girdInfoList2.findIndex(v => res.data.girdName2 === v.girdName)
}
}
})
}
}
}
</script>
<style lang="scss">
.pdgrid {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
background: url(https://cdn.cunwuyun.cn/dvcp/dv/pddv/middle-bg.png) no-repeat center;
background-size: contain;
.fade-enter-active, .fade-leave-active {
transition: opacity .3s ease-in-out;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
* {
box-sizing: border-box;
}
.pdgrid-grid__title {
position: absolute;
top: 40px;
left: 50%;
width: 271px;
height: 53px;
line-height: 53px;
text-align: center;
background: url(https://cdn.cunwuyun.cn/dvcp/dv/pddv/grid-title-sbg.png) no-repeat center;
background-size: 100% 100%;
cursor: pointer;
transform: translateX(-50%);
transition: opacity ease 0.3s;
&:hover {
opacity: 0.8;
}
h2 {
width: 182px;
margin: 0 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #FFFFFF;
font-size: 21px;
text-shadow: 0px 0px 13px rgb(59 182 255 / 80%);
background: #fff;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
.pdgrid-title {
position: absolute;
top: 200px;
left: 50%;
min-width: 640px;
height: 80px;
line-height: 80px;
text-align: center;
background: url(https://cdn.cunwuyun.cn/dvcp/dv/pddv/middle-titlebg.png) no-repeat center;
background-size: 100% 100%;
transform: translateX(-50%);
h2 {
color: #FFFFFF;
font-size: 22px;
white-space: nowrap;
text-shadow: 0px 0px 13px rgb(59 182 255 / 80%);
background: #fff;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
.pdgrid-body {
display: flex;
position: absolute;
justify-content: space-between;
bottom: 200px;
left: 0;
width: 100%;
padding: 0 112px;
.pdgrid-body__item {
display: flex;
flex-direction: column;
width: 200px;
height: 187px;
align-items: center;
padding-top: 71px;
cursor: pointer;
background: url(https://cdn.cunwuyun.cn/dvcp/dv/pddv/item-bg.png) no-repeat center;
background-size: 100% 100%;
transition: opacity ease 0.3s;
&:hover {
opacity: 0.8;
}
&:nth-of-type(2) {
position: relative;
top: 67px;
}
h2 {
font-size: 36px;
color: #FFFFFF;
text-shadow: 0px 0px 13px rgb(59 182 255 / 80%);
background: #fff;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
p {
max-width: 164px;
margin-top: 4px;
padding: 0 16px;
font-size: 16px;
color: #FFFFFF;
text-shadow: 0px 0px 13px rgb(59 182 255 / 80%);
background: #fff;
-webkit-background-clip: text;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-webkit-text-fill-color: transparent;
}
.bottom {
display: flex;
align-items: center;
i {
width: 0px;
height: 0px;
border: 6px solid transparent;
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
border-right-color: #FFCB42;
&.right {
width: 0px;
height: 0px;
border: 6px solid transparent;
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: #FFCB42;
border-right-color: transparent;
}
}
}
}
}
.grid-dialog {
position: fixed;
top: 0;
left: 0;
z-index: 111;
width: 100%;
height: 100%;
& > .mask {
position: absolute;
left: 0;
top: 0;
z-index: 1;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.grid-container {
display: flex;
position: absolute;
flex-direction: column;
left: 50%;
top: 50%;
z-index: 2;
width: 640px;
height: 640px;
background: rgba(7,13,41,0.9);
border: 1px solid #144662;
transform: translate(-50%, -50%);
& > h2 {
width: 100%;
height: 67px;
line-height: 67px;
padding: 0 20px;
text-align: center;
color: #FFFFFF;
font-size: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0px 0px 13px rgb(59 182 255 / 80%);
background: url(https://cdn.cunwuyun.cn/dvcp/dv/pddv/grid-title-bg.png) no-repeat center;
background-size: 100% 100%;
}
.grid-list {
flex: 1;
overflow-y: auto;
& > div {
height: 67px;
line-height: 67px;
padding: 0 20px;
text-align: center;
color: #FFFFFF;
font-size: 27px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all ease 0.5s;
&.grid-active {
background: linear-gradient(270deg, rgba(0,48,124,0) 0%, #00307C 16%, rgba(0,99,255,0.9100) 50%, rgba(0,48,124,0.8200) 87%, rgba(0,48,124,0) 100%);
box-shadow: inset 0px -1px 0px 0px rgba(16,34,54,1);
text-shadow: 0px 3px 5px rgba(0,0,0,0.5000);
}
&:hover {
background: linear-gradient(270deg, rgba(0,48,124,0) 0%, #00307C 16%, rgba(0,99,255,0.9100) 50%, rgba(0,48,124,0.8200) 87%, rgba(0,48,124,0) 100%);
box-shadow: inset 0px -1px 0px 0px rgba(16,34,54,1);
text-shadow: 0px 3px 5px rgba(0,0,0,0.5000);
}
}
}
}
}
}
</style>

View File

@@ -1,202 +1,158 @@
<template>
<div class="partyDvOrg" ref="container">
<div
class="partyDvOrg-wrapper"
ref="tree"
id="tree"
:style="{left: x, top: y, transform: `scale(${scale}) translate(-50%, -50%) `, 'transform-origin': `${0} ${0}`}">
<VueOkrTree
:props="props"
node-key="id"
ref="VueOkrTree"
:data="treeData">
</VueOkrTree>
<div class="partyDvOrg-wrapper" ref="tree" id="tree"
:style="{left: x, top: y, transform: `scale(${scale}) translate(-50%, -50%) `, 'transform-origin': `${0} ${0}`}">
<ai-okr-tree :props="props" node-key="id" :data="treeData"/>
</div>
</div>
</template>
<script>
import { VueOkrTree } from 'vue-okr-tree'
import 'vue-okr-tree/dist/vue-okr-tree.css'
import AiOkrTree from "./AiOkrTree/AiOkrTree";
export default {
name: 'AiDvPartyOrg',
props: ['instance'],
components: {
VueOkrTree
},
data () {
return {
scale: 1,
x: '50%',
y: '50%',
treeData: [],
props: {
label: 'name',
children: 'children'
}
}
},
mounted () {
this.bindEvent()
this.getPartyOrg()
},
destroyed () {
document.querySelector('body').removeEventListener('mousewheel', this.onMousewheel)
document.querySelector('body').removeEventListener('mouseup', this.onMouseUp)
document.querySelector('body').removeEventListener('mousedown', this.onMousedown)
document.querySelector('body').removeEventListener('mousemove', this.onMouseMove)
},
methods: {
bindEvent () {
document.querySelector('body').addEventListener('mousewheel', this.onMousewheel, true)
document.querySelector('body').addEventListener('mouseup', this.onMouseUp, true)
document.querySelector('body').addEventListener('mousedown', this.onMousedown, true)
document.querySelector('body').addEventListener('mousemove', this.onMouseMove, true)
},
onMousewheel (event) {
if (!event) return false
const elClass = event.target.className
if (elClass === 'tree' || elClass === 'middle' || (elClass && (elClass.indexOf('chart') > -1 || elClass.indexOf('user') > -1))) {
var dir = event.deltaY > 0 ? 'Up' : 'Down'
if (dir === 'Up') {
this.scale = this.scale - 0.2 <= 0.1 ? 0.1 : this.scale - 0.2
} else {
this.scale = this.scale + 0.2
}
}
return false
},
onMousedown (e) {
const elClass = e.target.className
if ((elClass && (elClass.indexOf('chart') > -1 || elClass.indexOf('user') > -1))) {
const left = document.querySelector('#tree').offsetLeft
const top = document.querySelector('#tree').offsetTop
this.isMove = true
this.offsetX = e.clientX - left
this.offsetY = e.clientY - top
}
},
onMouseMove (e) {
if (!this.isMove) return
this.x = (e.clientX - this.offsetX) + 'px'
this.y = (e.clientY - this.offsetY) + 'px'
},
onMouseUp () {
this.isMove = false
},
getPartyOrg () {
this.instance.post('/app/partyOrganization/queryPartyOrganizationServiceList').then(res => {
if (res.code === 0) {
this.treeData = res.data.filter(e => !e.parentId)
this.treeData.map(p => this.addChild(p, res.data.map(v => {
return {
...v,
name: v.name.substr(0, 12)
}
}), {parent: 'parentId'}))
this.$nextTick(() => {
this.autoScale()
})
}
})
},
autoScale () {
const treeWidth = this.$refs.tree.offsetWidth
const containerWidth = this.$refs.container.offsetWidth
this.scale = treeWidth < containerWidth ? 1 : containerWidth / treeWidth
this.x = '50%'
this.y = '50%'
export default {
name: 'AiDvPartyOrg',
components: {AiOkrTree},
props: ['instance'],
data() {
return {
scale: 1,
x: '50%',
y: '50%',
treeData: [],
props: {
label: 'name',
children: 'children'
}
}
},
mounted() {
this.bindEvent()
this.getPartyOrg()
},
destroyed() {
document.querySelector('body').removeEventListener('mousewheel', this.onMousewheel)
document.querySelector('body').removeEventListener('mouseup', this.onMouseUp)
document.querySelector('body').removeEventListener('mousedown', this.onMousedown)
document.querySelector('body').removeEventListener('mousemove', this.onMouseMove)
},
methods: {
bindEvent() {
document.querySelector('body').addEventListener('mousewheel', this.onMousewheel, true)
document.querySelector('body').addEventListener('mouseup', this.onMouseUp, true)
document.querySelector('body').addEventListener('mousedown', this.onMousedown, true)
document.querySelector('body').addEventListener('mousemove', this.onMouseMove, true)
},
onMousewheel(event) {
if (!event) return false
const elClass = event.target.className
if (elClass === 'tree' || elClass === 'middle' || (elClass && (elClass.indexOf('chart') > -1 || elClass.indexOf('user') > -1))) {
var dir = event.deltaY > 0 ? 'Up' : 'Down'
if (dir === 'Up') {
this.scale = this.scale - 0.2 <= 0.1 ? 0.1 : this.scale - 0.2
} else {
this.scale = this.scale + 0.2
}
}
return false
},
onMousedown(e) {
const elClass = e.target.className
if ((elClass && (elClass.indexOf('chart') > -1 || elClass.indexOf('user') > -1))) {
const left = document.querySelector('#tree').offsetLeft
const top = document.querySelector('#tree').offsetTop
this.isMove = true
this.offsetX = e.clientX - left
this.offsetY = e.clientY - top
}
},
onMouseMove(e) {
if (!this.isMove) return
this.x = (e.clientX - this.offsetX) + 'px'
this.y = (e.clientY - this.offsetY) + 'px'
},
onMouseUp() {
this.isMove = false
},
getPartyOrg() {
this.instance.post('/app/partyOrganization/queryPartyOrganizationServiceList').then(res => {
if (res.code === 0) {
this.treeData = res.data.filter(e => !e.parentId)
this.treeData.map(p => this.addChild(p, res.data.map(v => {
return {
...v,
name: v.name.substr(0, 12)
}
}), {parent: 'parentId'}))
this.$nextTick(() => {
this.autoScale()
})
}
})
},
autoScale() {
const treeWidth = this.$refs.tree.offsetWidth
const containerWidth = this.$refs.container.offsetWidth
this.scale = treeWidth < containerWidth ? 1 : containerWidth / treeWidth
this.x = '50%'
this.y = '50%'
}
}
}
</script>
<style lang="scss" scoped>
.partyDvOrg {
position: relative;
width: 100%;
height: 100%;
.partyDvOrg {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.partyDvOrg-wrapper {
display: flex;
position: absolute;
align-items: center;
left: 50%;
top: 50%;
padding: 20px;
overflow: hidden;
width: max-content;
height: 300%;
}
.partyDvOrg-wrapper {
display: flex;
position: absolute;
align-items: center;
left: 50%;
top: 50%;
padding: 20px;
::v-deep .org-chart-container {
color: #FFFFFF;
font-size: 16px;
.org-chart-node {
overflow: hidden;
width: max-content;
height: 300%;
}
::v-deep .org-chart-container {
color: #FFFFFF;
font-size: 16px;
.org-chart-node {
overflow: hidden;
.org-chart-node-label {
width: 40px;
height: 330px;
margin-right: 15px;
padding: 0 0;
box-shadow: 0 0 10px 4px rgba(188, 59, 0, 0.6) inset;
.org-chart-node-label-inner {
line-height: 1.3;
padding: 10px 0;
font-weight: 500;
writing-mode: vertical-rl;
text-align: center;
letter-spacing: 5px;
font-size: 18px;
font-family: MicrosoftYaHei-Bold, MicrosoftYaHei;
font-weight: bold;
color: #FFFFFF;
line-height: 24px;
text-shadow: 0px 2px 4px rgba(117, 9, 9, 0.2);
background: linear-gradient(180deg, #FFF6C7 0%, #FFC573 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
user-select: none;
}
}
&:last-child {
.org-chart-node-label {
margin-right: 0;
}
}
}
.is-root-label {
width: auto!important;
height: 40px!important;
line-height: 40px!important;
min-height: 40px!important;
text-align: center;
.org-chart-node-label {
width: 40px;
height: 330px;
margin-right: 15px;
padding: 0 0;
box-shadow: 0 0 10px 4px rgba(188, 59, 0, 0.6) inset;
.org-chart-node-label-inner {
padding: 0 30px!important;
writing-mode: horizontal-tb!important;
line-height: 1.3;
padding: 10px 0;
font-weight: 500;
writing-mode: vertical-rl;
text-align: center;
letter-spacing: 5px;
font-size: 18px;
font-family: MicrosoftYaHei-Bold, MicrosoftYaHei;
font-weight: bold;
@@ -206,19 +162,49 @@
background: linear-gradient(180deg, #FFF6C7 0%, #FFC573 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
user-select: none;
}
}
.org-chart-node-children:before, .org-chart-node:after, .org-chart-node:last-child:before,
.org-chart-node.is-leaf:before {
border-radius: 0;
border-color: #FFBA3E!important;
}
.vertical .org-chart-node:after, .vertical .org-chart-node:before {
border-radius: 0;
border-color: #FFBA3E!important;
&:last-child {
.org-chart-node-label {
margin-right: 0;
}
}
}
.is-root-label {
width: auto !important;
height: 40px !important;
line-height: 40px !important;
min-height: 40px !important;
text-align: center;
.org-chart-node-label-inner {
padding: 0 30px !important;
writing-mode: horizontal-tb !important;
font-size: 18px;
font-family: MicrosoftYaHei-Bold, MicrosoftYaHei;
font-weight: bold;
color: #FFFFFF;
line-height: 24px;
text-shadow: 0px 2px 4px rgba(117, 9, 9, 0.2);
background: linear-gradient(180deg, #FFF6C7 0%, #FFC573 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
.org-chart-node-children:before, .org-chart-node:after, .org-chart-node:last-child:before,
.org-chart-node.is-leaf:before {
border-radius: 0;
border-color: #FFBA3E !important;
}
.vertical .org-chart-node:after, .vertical .org-chart-node:before {
border-radius: 0;
border-color: #FFBA3E !important;
}
}
}
</style>

View File

@@ -27,7 +27,7 @@
<ai-monitor :src="data.src" v-else-if="data.type === 'monitor'" :type="data.monitorType"/>
<video style="width: 100%; height: 100%; object-fit: fill;" loop :src="data.src" autoplay v-else-if="data.type === 'video'"/>
<AiDvPartyOrg style="width: 100%; height: 100%;" v-else-if="data.type === 'partyOrg'" :instance="instance"/>
<ai-sprite v-else-if="/building/.test(data.type)" v-bind="data" is3D @init="mods[data.type]"/>
<!-- <ai-sprite v-else-if="/building/.test(data.type)" v-bind="data" is3D @init="mods[data.type]"/> -->
</ai-dv-panel>
</div>
</template>
@@ -41,14 +41,13 @@ import AiDvPanel from "../layout/AiDvPanel/AiDvPanel";
import AiDvDisplay from "../layout/AiDvDisplay/AiDvDisplay";
import AiDvSummary from "../layout/AiDvSummary/AiDvSummary";
import AiSprite from "./AiSprite";
import * as mods from "./AiSprite/mods";
export default {
name: 'AiDvRender',
props: ['data', 'index', 'theme', 'instance'],
components: {
AiSprite,
// AiSprite,
AiDvSummary,
AiDvDisplay,
AiDvPanel,
@@ -57,7 +56,7 @@ export default {
},
data() {
return {
mods,
// mods,
chartList,
map: null,
lib: null

View File

@@ -0,0 +1,732 @@
<template>
<div class="org-chart-container">
<div
ref="orgChartRoot"
class="org-chart-node-children"
:class="{
vertical: direction === 'vertical',
horizontal: direction === 'horizontal',
'show-collapsable': showCollapsable,
'one-branch': data.length === 1
}"
>
<OkrTreeNode
v-for="child in root.childNodes"
:node="child"
:root="root"
orkstyle
:show-collapsable="showCollapsable"
:label-width="labelWidth"
:label-height="labelHeight"
:renderContent="renderContent"
:nodeBtnContent="nodeBtnContent"
:selected-key="selectedKey"
:default-expand-all="defaultExpandAll"
:node-key="nodeKey"
:show-node-num="showNodeNum"
:key="getNodeKey(child)"
:props="props"
></OkrTreeNode>
</div>
</div>
</template>
<script>
import Vue from "vue";
import OkrTreeNode from "./OkrTreeNode.vue";
import TreeStore from "./model/tree-store.js";
import {getNodeKey} from "./model/util";
export default {
name: "AiOkrTree",
components: {
OkrTreeNode
},
provide() {
return {
okrEventBus: this.okrEventBus
};
},
props: {
data: {
// 源数据
required: true
},
leftData: {
// 源数据
type: Array
},
// 方向
direction: {
type: String,
default: "vertical"
},
// 子节点是否可折叠
showCollapsable: {
type: Boolean,
default: false
},
// 飞书 OKR 模式
onlyBothTree: {
type: Boolean,
default: false
},
orkstyle: {
type: Boolean,
default: false
},
// 树节点的内容区的渲染 Function
renderContent: Function,
// 展开节点的内容渲染 Function
nodeBtnContent: Function,
// 显示节点数
showNodeNum: Boolean,
// 树节点区域的宽度
labelWidth: [String, Number],
// 树节点区域的高度
labelHeight: [String, Number],
// 树节点的样式
labelClassName: [Function, String],
// 当前选中节点样式
currentLableClassName: [Function, String],
// 用来控制选择节点的字段名
selectedKey: String,
// 是否默认展开所有节点
defaultExpandAll: {
type: Boolean,
default: false
},
// 当前选中的节点
currentNodeKey: [String, Number],
// 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的
nodeKey: String,
defaultExpandedKeys: {
type: Array
},
filterNodeMethod: Function,
props: {
default() {
return {
leftChildren: "leftChildren",
children: "children",
label: "label",
disabled: "disabled"
};
}
},
// 动画
animate: {
type: Boolean,
default: false
},
animateName: {
type: String,
default: "okr-zoom-in-center"
},
animateDuration: {
type: Number,
default: 200
}
},
computed: {
ondeClass() {
return {
findNode: null
};
}
},
data() {
return {
okrEventBus: new Vue(),
store: null,
root: null
};
},
created() {
this.isTree = true;
this.store = new TreeStore({
key: this.nodeKey,
data: Object.freeze(this.data),
leftData: this.leftData,
props: this.props,
defaultExpandedKeys: this.defaultExpandedKeys,
showCollapsable: this.showCollapsable,
currentNodeKey: this.currentNodeKey,
defaultExpandAll: this.defaultExpandAll,
filterNodeMethod: this.filterNodeMethod,
labelClassName: this.labelClassName,
currentLableClassName: this.currentLableClassName,
onlyBothTree: this.onlyBothTree,
direction: this.direction,
animate: this.animate,
animateName: this.animateName
});
this.root = this.store.root;
},
watch: {
data(newVal) {
this.store.setData(newVal);
},
defaultExpandedKeys(newVal) {
this.store.defaultExpandedKeys = newVal;
this.store.setDefaultExpandedKeys(newVal);
}
},
methods: {
filter(value) {
if (!this.filterNodeMethod)
throw new Error("[Tree] filterNodeMethod is required when filter");
this.store.filter(value);
if (this.onlyBothTree) {
this.store.filter(value, "leftChildNodes");
}
},
getNodeKey(node) {
return getNodeKey(this.nodeKey, node.data);
},
// 通过 node 设置某个节点的当前选中状态
setCurrentNode(node) {
if (!this.nodeKey)
throw new Error("[Tree] nodeKey is required in setCurrentNode");
this.store.setUserCurrentNode(node);
},
// 根据 data 或者 key 拿到 Tree 组件中的 node
getNode(data) {
return this.store.getNode(data);
},
// 通过 key 设置某个节点的当前选中状态
setCurrentKey(key) {
if (!this.nodeKey)
throw new Error("[Tree] nodeKey is required in setCurrentKey");
this.store.setCurrentNodeKey(key);
},
remove(data) {
this.store.remove(data);
},
// 获取当前被选中节点的 data
getCurrentNode() {
const currentNode = this.store.getCurrentNode();
return currentNode ? currentNode.data : null;
},
getCurrentKey() {
if (!this.nodeKey)
throw new Error("[Tree] nodeKey is required in getCurrentKey");
const currentNode = this.getCurrentNode();
return currentNode ? currentNode[this.nodeKey] : null;
},
append(data, parentNode) {
this.store.append(data, parentNode);
},
insertBefore(data, refNode) {
this.store.insertBefore(data, refNode);
},
insertAfter(data, refNode) {
this.store.insertAfter(data, refNode);
},
updateKeyChildren(key, data) {
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild');
this.store.updateChildren(key, data);
}
}
};
</script>
<style>
@import "model/transition.css";
* {
margin: 0;
padding: 0;
}
.org-chart-container {
display: block;
width: 100%;
text-align: center;
}
.vertical .org-chart-node-children {
position: relative;
padding-top: 20px;
transition: all 0.5s;
}
.vertical .org-chart-node {
float: left;
text-align: center;
list-style-type: none;
position: relative;
padding: 20px 5px 0 5px;
transition: all 0.5s;
}
/*使用 ::before 和 ::after 绘制连接器*/
.vertical .org-chart-node::before,
.vertical .org-chart-node::after {
content: "";
position: absolute;
top: 0;
right: 50%;
width: 50%;
border-top: 1px solid #ccc;
height: 20px;
}
.vertical .org-chart-node::after {
right: auto;
left: 50%;
border-left: 1px solid #ccc;
}
/*我们需要从没有任何兄弟元素的元素中删除左右连接器*/
.vertical.one-branch > .org-chart-node::after,
.vertical.one-branch > .org-chart-node::before {
display: none;
}
/*从单个子节点的顶部移除空格*/
.vertical.one-branch > .org-chart-node {
padding-top: 0;
}
/*从第一个子节点移除左连接器,从最后一个子节点移除右连接器*/
.vertical .org-chart-node:first-child::before,
.vertical .org-chart-node:last-child::after {
border: 0 none;
}
/*将垂直连接器添加回最后的节点*/
.vertical .org-chart-node:last-child::before {
border-right: 1px solid #ccc;
border-radius: 0 5px 0 0;
}
.vertical .org-chart-node:only-child:before {
border-radius: 0 0px 0 0;
margin-right: -1px;
}
.vertical .org-chart-node:first-child::after {
border-radius: 5px 0 0 0;
}
.vertical .org-chart-node.is-leaf {
padding-top: 20px;
padding-bottom: 20px;
}
.vertical .org-chart-node.is-leaf::before {
content: "";
display: block;
height: 20px;
}
.vertical .org-chart-node.is-leaf .org-chart-node-label::after {
display: none;
}
/*从父节点添加向下的连接器了*/
.vertical .org-chart-node-children::before {
content: "";
position: absolute;
top: 0;
left: 50%;
border-left: 1px solid #ccc;
width: 0;
height: 20px;
}
.vertical .org-chart-node-label {
position: relative;
display: inline-block;
}
.vertical .org-chart-node-label .org-chart-node-label-inner {
box-shadow: 0 1px 10px rgba(31, 35, 41, 0.08);
display: inline-block;
padding: 10px;
margin: 0px;
font-size: 16px;
word-break: break-all;
}
.vertical .org-chart-node-label .org-chart-node-label-inner:hover {
box-shadow: 0 1px 14px rgba(31, 35, 41, 0.12);
cursor: pointer;
}
.vertical .org-chart-node-label .org-chart-node-btn {
position: absolute;
top: 100%;
left: 50%;
width: 20px;
height: 20px;
z-index: 10;
margin-left: -11px;
margin-top: 9px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 50%;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: all 0.35s ease;
}
.vertical .org-chart-node-label .org-chart-node-btn:hover {
transform: scale(1.15);
cursor: pointer;
}
.vertical .org-chart-node-label .org-chart-node-btn::before,
.vertical .org-chart-node-label .org-chart-node-btn::after {
content: "";
position: absolute;
}
.vertical .org-chart-node-label .org-chart-node-btn::before {
top: 50%;
left: 4px;
right: 4px;
height: 0;
border-top: 1px solid #ccc;
}
.vertical .org-chart-node-label .org-chart-node-btn::after {
top: 4px;
left: 50%;
bottom: 4px;
width: 0;
border-left: 1px solid #ccc;
}
.vertical .org-chart-node-label .expanded.org-chart-node-btn::after {
display: none;
}
.vertical .org-chart-node.collapsed .org-chart-node-label {
position: relative;
}
.vertical .org-chart-node.collapsed .org-chart-node-label::after {
content: "";
position: absolute;
top: 100%;
left: 0;
width: 50%;
height: 20px;
border-right: 1px solid #ddd;
}
.horizontal .org-chart-node-children,
.horizontal .org-chart-node-left-children {
position: relative;
padding-left: 20px;
transition: all 0.5s;
}
.horizontal .org-chart-node-left-children {
padding-right: 20px;
}
.horizontal .org-chart-node:not(.is-left-child-node) {
display: flex;
align-items: center;
position: relative;
padding: 0px 5px 0 20px;
transition: all 0.5s;
}
.horizontal .is-left-child-node {
display: flex;
position: relative;
justify-content: flex-end;
align-items: center;
}
.horizontal .is-left-child-node {
padding: 0px 20px 0 5px;
}
/*使用 ::before 和 ::after 绘制连接器*/
.horizontal .org-chart-node:not(.is-left-child-node):before,
.horizontal .org-chart-node:not(.is-left-child-node)::after {
content: "";
position: absolute;
border-left: 1px solid #ccc;
top: 0;
left: 0;
width: 20px;
height: 50%;
}
.horizontal .is-left-child-node:before,
.horizontal .is-left-child-node::after {
content: "";
position: absolute;
border-right: 1px solid #ccc;
top: 0;
right: 0;
width: 20px;
height: 50%;
}
.horizontal .org-chart-node:not(.is-left-child-node):after {
top: 50%;
border-top: 1px solid #ccc;
}
.horizontal .is-left-child-node:after {
top: 50%;
border-top: 1px solid #ccc;
}
/*我们需要从没有任何兄弟元素的元素中删除左右连接器*/
.horizontal.one-branch > .org-chart-node::after,
.horizontal.one-branch > .org-chart-node::before {
display: none;
}
/*从单个子节点的顶部移除空格*/
.horizontal.one-branch > .org-chart-node {
padding-left: 0;
}
/*从第一个子节点移除左连接器,从最后一个子节点移除右连接器*/
.horizontal .org-chart-node:first-child::before,
.horizontal .org-chart-node:last-child::after {
border: 0 none;
}
/*将垂直连接器添加回最后的节点*/
.horizontal .org-chart-node:not(.is-left-child-node):not(.is-not-child):last-child::before {
border-bottom: 1px solid #ccc;
border-radius: 0 0px 0 5px;
}
.horizontal .is-left-child-node:last-child::before {
border-bottom: 1px solid #ccc;
border-radius: 0 0px 5px 0px;
}
.horizontal .org-chart-node:only-child::before {
border-radius: 0 0px 0 0px !important;
border-color: #ccc;
}
.horizontal .org-chart-node:not(.is-left-child-node):first-child::after {
border-radius: 5px 0px 0 0;
}
.horizontal .is-left-child-node:first-child::after {
border-radius: 0 5px 0px 0px;
}
.horizontal .org-chart-node.is-leaf {
position: relative;
padding-left: 20px;
padding-right: 20px;
}
.horizontal .org-chart-node.is-leaf::before {
content: "";
display: block;
}
.horizontal .org-chart-node.is-leaf .org-chart-node-label::after,
.horizontal .is-left-child-node.is-leaf .org-chart-node-label::before {
display: none;
}
.horizontal .is-left-child-node:last-child::after {
/* border-bottom: 1px solid green; */
border-radius: 0 0px 5px 0px;
}
.horizontal .is-left-child-node:only-child::after {
border-radius: 0 0px 0 0px;
}
.horizontal .is-left-child-node.is-leaf {
position: relative;
padding-left: 20px;
padding-right: 20px;
}
.horizontal .is-left-child-node.is-leaf::before {
content: "";
display: block;
}
.horizontal .is-left-child-node .org-chart-node-label::after {
display: none;
}
/*从父节点添加向下的连接器了*/
.horizontal .org-chart-node-children::before,
.horizontal .org-chart-node-left-children::before {
content: "";
position: absolute;
left: 0;
top: 50%;
border-top: 1px solid #ccc;
width: 12px;
height: 10px;
}
.horizontal .org-chart-node-children::before {
width: 20px;
}
.horizontal .org-chart-node-left-children::before {
left: calc(100% - 11px);
}
.horizontal > .only-both-tree-node > .org-chart-node-left-children::before {
display: none;
}
.horizontal .org-chart-node-label {
position: relative;
display: inline-block;
}
.horizontal .org-chart-node-label .org-chart-node-label-inner {
box-shadow: 0 1px 10px rgba(31, 35, 41, 0.08);
display: inline-block;
padding: 10px;
margin: 10px 0;
font-size: 16px;
word-break: break-all;
}
.horizontal .org-chart-node-label .org-chart-node-label-inner:hover {
box-shadow: 0 1px 14px rgba(31, 35, 41, 0.12);
cursor: pointer;
}
.horizontal .org-chart-node-label .org-chart-node-btn {
position: absolute;
left: 100%;
top: 50%;
width: 20px;
height: 20px;
z-index: 10;
margin-top: -11px;
margin-left: 9px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 50%;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: all 0.35s ease;
}
.horizontal .org-chart-node-label .org-chart-node-btn:hover,
.horizontal .org-chart-node-label .org-chart-node-left-btn:hover {
transform: scale(1.15);
}
.horizontal .org-chart-node-label .org-chart-node-btn::before,
.horizontal .org-chart-node-label .org-chart-node-btn::after,
.horizontal .org-chart-node-label .org-chart-node-left-btn::before,
.horizontal .org-chart-node-label .org-chart-node-left-btn::after {
content: "";
position: absolute;
}
.horizontal .org-chart-node-label .org-chart-node-btn::before,
.horizontal .org-chart-node-label .org-chart-node-left-btn::before {
top: 50%;
left: 4px;
right: 3px;
border-top: 1px solid #ccc;
height: 0;
transform: translateY(-50%);
}
.horizontal .org-chart-node-label .org-chart-node-btn::after,
.horizontal .org-chart-node-label .org-chart-node-left-btn::after {
top: 4px;
left: 50%;
bottom: 4px;
width: 0;
border-left: 1px solid #ccc;
}
.horizontal .org-chart-node-label .expanded.org-chart-node-btn::after,
.horizontal .org-chart-node-label .expanded.org-chart-node-left-btn::after {
display: none;
}
.horizontal .org-chart-node-label .org-chart-node-left-btn {
position: absolute;
right: 100%;
top: 50%;
width: 20px;
height: 20px;
z-index: 10;
margin-top: -11px;
margin-right: 9px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 50%;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: all 0.35s ease;
}
.horizontal .org-chart-node.collapsed .org-chart-node-label,
.horizontal .is-left-child-node.collapsed .org-chart-node-label {
position: relative;
}
.horizontal .org-chart-node.collapsed .org-chart-node-label::after,
.horizontal .is-left-child-node.collapsed .org-chart-node-label::before {
content: "";
border-bottom: 1px solid #ccc;
position: absolute;
top: 0;
left: 100%;
height: 50%;
width: 10px;
}
.horizontal .org-chart-node .is-root-label.is-not-right-child::after,
.horizontal .org-chart-node .is-root-label.is-not-left-child::before {
display: none !important;
}
/* .horizontal .org-chart-node.collapsed .is-root-label.is-right-child::after,
.horizontal .org-chart-node.collapsed .is-root-label.is-left-child::before {
display: block;
} */
.horizontal .is-left-child-node.collapsed .org-chart-node-label::before {
left: -10px;
}
.horizontal .only-both-tree-node > .org-chart-node-label::before {
content: "";
border-bottom: 1px solid #ccc;
position: absolute;
top: 0;
right: 100%;
height: 50%;
width: 20px;
}
.org-chart-node-children .org-chart-node-btn-text {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 50%;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #909090;
z-index: 2;
}
</style>

View File

@@ -0,0 +1,386 @@
<template>
<div
class="org-chart-node"
@contextmenu="$event => this.handleContextMenu($event)"
v-if="node.visible"
:class="{
collapsed: !node.leftExpanded || !node.expanded,
'is-leaf': isLeaf,
'is-current': node.isCurrent,
'is-left-child-node': isLeftChildNode,
'is-not-child': node.level === 1 && node.childNodes.length <= 0 && leftChildNodes.length <= 0,
'only-both-tree-node': node.level === 1 && tree.store.onlyBothTree
}"
>
<transition :duration="animateDuration" :name="animateName">
<div
class="org-chart-node-left-children"
v-if="showLeftChildNode"
v-show="node.leftExpanded"
>
<OkrTreeNode
v-for="child in leftChildNodes"
:show-collapsable="showCollapsable"
:node="child"
:label-width="labelWidth"
:label-height="labelHeight"
:renderContent="renderContent"
:nodeBtnContent="nodeBtnContent"
:selected-key="selectedKey"
:node-key="nodeKey"
:key="getNodeKey(child)"
:props="props"
:show-node-num="showNodeNum"
is-left-child-node
></OkrTreeNode>
</div>
</transition>
<div class="org-chart-node-label"
:class="{
'is-root-label': node.level === 1,
'is-not-right-child': node.level === 1 && node.childNodes.length <= 0,
'is-not-left-child': node.level === 1 && leftChildNodes.length <= 0
}"
>
<div
v-if="showNodeLeftBtn && leftChildNodes.length > 0"
class="org-chart-node-left-btn"
:class="{ expanded: node.leftExpanded }"
@click="handleBtnClick('left')">
<template v-if="showNodeNum" >
<span v-if="!node.leftExpanded" class="org-chart-node-btn-text">
{{ (node.level === 1 && leftChildNodes.length > 0) ? leftChildNodes.length : node.childNodes.length }}
</span>
</template>
<node-btn-content v-else :node="node">
<slot>
</slot>
</node-btn-content>
</div>
<div
class="org-chart-node-label-inner"
@click="handleNodeClick"
:class="computeLabelClass"
:style="computeLabelStyle"
>
<node-content :node="node">
<slot>
{{ node.label }}
</slot>
</node-content>
</div>
<div
v-if="showNodeBtn && !isLeftChildNode"
class="org-chart-node-btn"
:class="{ expanded: node.expanded }"
@click="handleBtnClick('right')">
<template v-if="showNodeNum">
<span v-if="!node.expanded " class="org-chart-node-btn-text">
{{ node.childNodes.length }}
</span>
</template>
<node-btn-content v-else :node="node">
<slot>
<!-- <div class="org-chart-node-btn-text">10</div> -->
</slot>
</node-btn-content>
</div>
</div>
<transition :duration="animateDuration" :name="animateName">
<div
class="org-chart-node-children"
v-if="!isLeftChildNode && node.childNodes && node.childNodes.length > 0"
v-show="node.expanded"
>
<OkrTreeNode
v-for="child in node.childNodes"
:show-collapsable="showCollapsable"
:node="child"
:label-width="labelWidth"
:label-height="labelHeight"
:renderContent="renderContent"
:nodeBtnContent="nodeBtnContent"
:selected-key="selectedKey"
:node-key="nodeKey"
:key="getNodeKey(child)"
:show-node-num='showNodeNum'
:props="props"
></OkrTreeNode>
</div>
</transition>
</div>
</template>
<script>
import { getNodeKey } from "./model/util";
export default {
name: "OkrTreeNode",
inject: ["okrEventBus"],
props: {
props: {},
node: {
default() {
return {};
}
},
root: {
default() {
return {};
}
},
// 子节点是否可折叠
showCollapsable: {
type: Boolean,
default: false
},
// 判断是否是左子树的节点,样式需要改
isLeftChildNode: {
type: Boolean,
default: false
},
// 树节点的内容区的渲染 Function
renderContent: Function,
// 展开节点的内容渲染 Function
nodeBtnContent: Function,
// 显示节点数
showNodeNum: Boolean,
// 树节点区域的宽度
labelWidth: [String, Number],
// 树节点区域的高度
labelHeight: [String, Number],
// 用来控制选择节点的字段名
selectedKey: String,
// 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的
nodeKey: String
},
components: {
NodeContent: {
props: {
node: {
required: true
}
},
render(h) {
const parent = this.$parent;
if (parent._props.renderContent) {
return parent._props.renderContent(h, this.node);
} else {
return this.$scopedSlots.default(this.node);
}
}
},
// 渲染展开节点的样式
NodeBtnContent: {
props: {
node: {
required: true
}
},
render(h) {
const parent = this.$parent;
if (parent._props.nodeBtnContent) {
return parent._props.nodeBtnContent(h, this.node);
} else {
if (this.$scopedSlots.default) {
return this.$scopedSlots.default(this.node);
}
}
}
}
},
computed: {
isLeaf () {
if (this.node.level === 1) {
if (this.leftChildNodes.length == 0 && this.node.childNodes.length == 0) {
return true
} else {
return false
}
} else {
return this.node.isLeaf
}
},
leftChildNodes() {
if (this.tree.store.onlyBothTree) {
if (this.isLeftChildNode) {
return this.node.childNodes;
} else {
return this.node.leftChildNodes;
}
}
return [];
},
animateName() {
if (this.tree.store.animate) {
return this.tree.store.animateName;
}
return "";
},
animateDuration() {
if (this.tree.store.animate) {
return this.tree.store.animateDuration;
}
return 0;
},
// 是否显示展开按钮
showNodeBtn() {
if (this.isLeftChildNode) {
return (
(this.tree.store.direction === "horizontal" &&
this.showCollapsable &&
this.leftChildNodes.length > 0) || this.level === 1
);
}
return (
this.showCollapsable &&
this.node.childNodes &&
this.node.childNodes.length > 0
)
},
showNodeLeftBtn() {
return (
(this.tree.store.direction === "horizontal" &&
this.showCollapsable &&
this.leftChildNodes.length > 0)
)
},
// 节点的宽度
computeLabelStyle() {
let { labelWidth = "auto", labelHeight = "auto" } = this;
if (typeof labelWidth === "number") {
labelWidth = `${labelWidth}px`;
}
if (typeof labelHeight === "number") {
labelHeight = `${labelHeight}px`;
}
return {
width: labelWidth,
height: labelHeight
};
},
computeLabelClass() {
let clsArr = [];
const store = this.tree.store;
if (store.labelClassName) {
if (typeof store.labelClassName === "function") {
clsArr.push(store.labelClassName(this.node));
} else {
clsArr.push(store.labelClassName);
}
}
if (store.currentLableClassName && this.node.isCurrent) {
if (typeof store.currentLableClassName === "function") {
clsArr.push(store.currentLableClassName(this.node));
} else {
clsArr.push(store.currentLableClassName);
}
}
if (this.node.isCurrent) {
clsArr.push("is-current");
}
return clsArr;
},
computNodeStyle() {
return {
display: this.node.expanded ? "" : "none"
};
},
computLeftNodeStyle() {
return {
display: this.node.leftExpanded ? "" : "none"
};
},
// 是否显示左子数
showLeftChildNode() {
return (
this.tree.onlyBothTree &&
this.tree.store.direction === "horizontal" &&
this.leftChildNodes &&
this.leftChildNodes.length > 0
);
}
},
watch: {
"node.expanded"(val) {
// this.$nextTick(() => this.expanded = val);
},
"node.leftExpanded"(val) {
// this.$nextTick(() => this.expanded = val);
}
},
data() {
return {
// node 的展开状态
expanded: false,
tree: null
};
},
created() {
const parent = this.$parent;
if (parent.isTree) {
this.tree = parent;
} else {
this.tree = parent.tree;
}
const tree = this.tree;
if (!tree) {
console.warn("Can not find node's tree.");
}
},
methods: {
getNodeKey(node) {
return getNodeKey(this.nodeKey, node.data);
},
handleNodeClick() {
const store = this.tree.store;
store.setCurrentNode(this.node);
this.tree.$emit("node-click", this.node.data, this.node, this);
},
handleBtnClick(isLeft) {
isLeft = isLeft === "left";
const store = this.tree.store;
// 表示是OKR飞书模式
if (store.onlyBothTree) {
if (isLeft) {
if (this.node.leftExpanded) {
this.node.leftExpanded = false;
this.tree.$emit("node-collapse", this.node.data, this.node, this);
} else {
this.node.leftExpanded = true;
this.tree.$emit("node-expand", this.node.data, this.node, this);
}
return;
}
}
if (this.node.expanded) {
this.node.collapse();
this.tree.$emit("node-collapse", this.node.data, this.node, this);
} else {
if (this.node.parent.childNodes && this.node.parent.childNodes.length) {
this.node.parent.childNodes.forEach(e => {
e.collapse()
})
}
this.node.expand();
this.tree.$emit("node-expand", this.node.data, this.node, this);
}
},
handleContextMenu(event) {
if (
this.tree._events["node-contextmenu"] &&
this.tree._events["node-contextmenu"].length > 0
) {
event.stopPropagation();
event.preventDefault();
}
this.tree.$emit(
"node-contextmenu",
event,
this.node.data,
this.node,
this
);
}
}
};
</script>

View File

@@ -0,0 +1,14 @@
export default function(target) {
for (let i = 1, j = arguments.length; i < j; i++) {
let source = arguments[i] || {};
for (let prop in source) {
if (source.hasOwnProperty(prop)) {
let value = source[prop];
if (value !== undefined) {
target[prop] = value;
}
}
}
}
return target;
}

View File

@@ -0,0 +1,254 @@
import { markNodeData, NODE_KEY } from './util';
import objectAssign from './merge';
const getPropertyFromData = function (node, prop) {
const props = node.store.props;
const data = node.data || {};
const config = props[prop];
if (typeof config === 'function') {
return config(data, node);
} else if (typeof config === 'string') {
return data[config];
} else if (typeof config === 'undefined') {
const dataProp = data[prop];
return dataProp === undefined ? '' : dataProp;
}
};
let nodeIdSeed = 0;
export default class Node {
constructor(options, isLeftChild = false) {
this.isLeftChild = isLeftChild;
this.id = nodeIdSeed++;
this.data = null;
this.expanded = false;
this.leftExpanded = false;
this.isCurrent = false;
this.visible = true;
this.parent = null;
for (let name in options) {
if (options.hasOwnProperty(name)) {
this[name] = options[name];
}
}
this.level = 0;
this.childNodes = [];
this.leftChildNodes = [];
if (this.parent) {
this.level = this.parent.level + 1;
}
const store = this.store;
if (!store) {
throw new Error('[Node]store is required!');
}
store.registerNode(this);
if (this.data) {
this.setData(this.data, isLeftChild);
if (store.defaultExpandAll || !store.showCollapsable) {
this.expanded = true;
this.leftExpanded = true;
}
}
if (!Array.isArray(this.data)) {
markNodeData(this, this.data);
}
if (!this.data) return;
const defaultExpandedKeys = store.defaultExpandedKeys;
const key = store.key;
if (
key &&
defaultExpandedKeys &&
defaultExpandedKeys.indexOf(this.key) !== -1
) {
this.expand(null, true);
}
if (
key &&
store.currentNodeKey !== undefined &&
this.key === store.currentNodeKey
) {
store.currentNode = this;
store.currentNode.isCurrent = true;
}
this.updateLeafState();
}
setData(data, isLeftChild) {
if (!Array.isArray(data)) {
markNodeData(this, data);
}
const store = this.store;
this.data = data;
this.childNodes = [];
let children;
if (this.level === 0 && this.data instanceof Array) {
children = this.data;
} else {
children = getPropertyFromData(this, 'children') || [];
}
for (let i = 0, j = children.length; i < j; i++) {
this.insertChild({ data: children[i] }, null, null, isLeftChild);
}
}
get key() {
const nodeKey = this.store.key;
if (this.data) return this.data[nodeKey];
return null;
}
get label() {
return getPropertyFromData(this, 'label');
}
// 是否是 OKR 飞书模式
hasLeftChild() {
const store = this.store;
return store.onlyBothTree && store.direction === 'horizontal';
}
insertChild(child, index, batch, isLeftChild) {
if (!child) throw new Error('insertChild error: child is required.');
if (!(child instanceof Node)) {
if (!batch) {
const children = this.getChildren(true);
if (children.indexOf(child.data) === -1) {
if (index === undefined || index === null || index < 0) {
children.push(child.data);
} else {
children.splice(index, 0, child.data);
}
}
}
objectAssign(child, {
parent: this,
store: this.store,
});
child = new Node(child, isLeftChild);
}
child.level = this.level + 1;
if (index === undefined || index === null || index < 0) {
this.childNodes.push(child);
} else {
this.childNodes.splice(index, 0, child);
}
this.updateLeafState();
}
getChildren(forceInit = false) {
// this is data
if (this.level === 0) return this.data;
const data = this.data;
if (!data) return null;
const props = this.store.props;
let children = 'children';
if (props) {
children = props.children || 'children';
}
if (data[children] === undefined) {
data[children] = null;
}
if (forceInit && !data[children]) {
data[children] = [];
}
return data[children];
}
updateLeafState() {
if (
this.store.lazy === true &&
this.loaded !== true &&
typeof this.isLeafByUser !== 'undefined'
) {
this.isLeaf = this.isLeafByUser;
return;
}
const childNodes = this.childNodes;
if (
!this.store.lazy ||
(this.store.lazy === true && this.loaded === true)
) {
this.isLeaf = !childNodes || childNodes.length === 0;
return;
}
this.isLeaf = false;
}
updateLeftLeafState() {
if (
this.store.lazy === true &&
this.loaded !== true &&
typeof this.isLeafByUser !== 'undefined'
) {
this.isLeaf = this.isLeafByUser;
return;
}
const childNodes = this.leftChildNodes;
if (
!this.store.lazy ||
(this.store.lazy === true && this.loaded === true)
) {
this.isLeaf = !childNodes || childNodes.length === 0;
return;
}
this.isLeaf = false;
}
// 节点的收起
collapse() {
this.expanded = false;
}
// 节点的展开
expand(callback, expandParent) {
const done = () => {
if (expandParent) {
let parent = this.parent;
while (parent.level > 0) {
parent.isLeftChild
? (parent.leftExpanded = true)
: (parent.expanded = true);
parent = parent.parent;
}
}
this.isLeftChild ? (this.leftExpanded = true) : (this.expanded = true);
if (callback) callback();
};
done();
}
removeChild(child) {
const children = this.getChildren() || [];
const dataIndex = children.indexOf(child.data);
if (dataIndex > -1) {
children.splice(dataIndex, 1);
}
const index = this.childNodes.indexOf(child);
if (index > -1) {
this.store && this.store.deregisterNode(child);
child.parent = null;
this.childNodes.splice(index, 1);
}
this.updateLeafState();
}
insertBefore(child, ref) {
let index;
if (ref) {
index = this.childNodes.indexOf(ref);
}
this.insertChild(child, index);
}
insertAfter(child, ref) {
let index;
if (ref) {
index = this.childNodes.indexOf(ref);
if (index !== -1) index += 1;
}
this.insertChild(child, index);
}
}

View File

@@ -0,0 +1 @@
.okr-fade-in-enter,.okr-fade-in-leave-active,.okr-fade-in-linear-enter,.okr-fade-in-linear-leave,.okr-fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.okr-fade-in-linear-enter-active,.okr-fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.okr-fade-in-enter-active,.okr-fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.okr-zoom-in-center-enter-active,.okr-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.okr-zoom-in-center-enter,.okr-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.okr-zoom-in-top-enter-active,.okr-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center top;transform-origin:center top}.okr-zoom-in-top-enter,.okr-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.okr-zoom-in-bottom-enter-active,.okr-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center bottom;transform-origin:center bottom}.okr-zoom-in-bottom-enter,.okr-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.okr-zoom-in-left-enter-active,.okr-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:top left;transform-origin:top left}.okr-zoom-in-left-enter,.okr-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.collapse-transition{-webkit-transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out;transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.horizontal-collapse-transition{-webkit-transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out;transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.okr-list-enter-active,.okr-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.okr-list-enter,.okr-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.okr-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}

View File

@@ -0,0 +1,176 @@
import Node from "./node";
import { getNodeKey } from "./util";
export default class TreeStore {
constructor(options) {
this.currentNode = null;
this.currentNodeKey = null;
for (let option in options) {
if (options.hasOwnProperty(option)) {
this[option] = options[option];
}
}
this.nodesMap = {};
this.root = new Node(
{
data: this.data,
store: this
},
false
);
if (this.root.store.onlyBothTree) {
if (!this.leftData)
throw new Error("[Tree] leftData is required in onlyBothTree");
if (this.leftData) {
this.isLeftChilds = new Node(
{
data: this.leftData,
store: this
},
true
);
if (this.isLeftChilds) {
this.root.childNodes[0].leftChildNodes = this.isLeftChilds.childNodes[0].childNodes;
this.root.childNodes[0].leftExpanded = this.isLeftChilds.childNodes[0].leftExpanded;
}
}
}
}
filter(value, childName = "childNodes") {
this.filterRight(value, childName);
}
// 过滤默认节点
filterRight(value, childName) {
const filterNodeMethod = this.filterNodeMethod;
const traverse = function(node, childName) {
let childNodes;
if (node.root) {
childNodes = node.root.childNodes[0][childName];
} else {
childNodes = node.childNodes;
}
childNodes.forEach(child => {
child.visible = filterNodeMethod.call(child, value, child.data, child);
traverse(child, childName);
});
if (!node.visible && childNodes.length) {
let allHidden = true;
allHidden = !childNodes.some(child => child.visible);
if (node.root) {
node.root.visible = allHidden === false;
} else {
node.visible = allHidden === false;
}
}
if (!value) return;
if (node.visible) node.expand();
};
traverse(this, childName);
}
registerNode(node) {
const key = this.key;
if (!key || !node || !node.data) return;
const nodeKey = node.key;
if (nodeKey !== undefined) this.nodesMap[node.key] = node;
}
deregisterNode(node) {
const key = this.key;
if (!key || !node || !node.data) return;
node.childNodes.forEach(child => {
this.deregisterNode(child);
});
delete this.nodesMap[node.key];
}
setData(newVal) {
const instanceChanged = newVal !== this.root.data;
if (instanceChanged) {
this.root.setData(newVal);
} else {
this.root.updateChildren();
}
}
updateChildren(key, data) {
const node = this.nodesMap[key];
if (!node) return;
const childNodes = node.childNodes;
for (let i = childNodes.length - 1; i >= 0; i--) {
const child = childNodes[i];
this.remove(child.data);
}
for (let i = 0, j = data.length; i < j; i++) {
const child = data[i];
this.append(child, node.data);
}
}
getNode(data) {
if (data instanceof Node) return data;
const key = typeof data !== "object" ? data : getNodeKey(this.key, data);
return this.nodesMap[key] || null;
}
setDefaultExpandedKeys(keys) {
keys = keys || [];
this.defaultExpandedKeys = keys;
keys.forEach(key => {
const node = this.getNode(key);
if (node) node.expand(null, true);
});
}
setCurrentNode(currentNode) {
const prevCurrentNode = this.currentNode;
if (prevCurrentNode) {
prevCurrentNode.isCurrent = false;
}
this.currentNode = currentNode;
this.currentNode.isCurrent = true;
}
setUserCurrentNode(node) {
const key = node.key;
const currNode = this.nodesMap[key];
this.setCurrentNode(currNode);
}
setCurrentNodeKey(key) {
if (key === null || key === undefined) {
this.currentNode && (this.currentNode.isCurrent = false);
this.currentNode = null;
return;
}
const node = this.getNode(key);
if (node) {
this.setCurrentNode(node);
}
}
getCurrentNode() {
return this.currentNode;
}
remove(data) {
const node = this.getNode(data);
if (node && node.parent) {
if (node === this.currentNode) {
this.currentNode = null;
}
node.parent.removeChild(node);
}
}
append(data, parentData) {
const parentNode = parentData ? this.getNode(parentData) : this.root;
if (parentNode) {
parentNode.insertChild({ data });
}
}
insertBefore(data, refData) {
const refNode = this.getNode(refData);
refNode.parent.insertBefore({ data }, refNode);
}
insertAfter(data, refData) {
const refNode = this.getNode(refData);
refNode.parent.insertAfter({ data }, refNode);
}
}

View File

@@ -0,0 +1,27 @@
export const NODE_KEY = "$treeNodeId";
export const markNodeData = function(node, data) {
if (!data || data[NODE_KEY]) return;
Object.defineProperty(data, NODE_KEY, {
value: node.id,
enumerable: false,
configurable: false,
writable: false
});
};
export const getNodeKey = function(key, data) {
if (!key) return data[NODE_KEY];
return data[key];
};
export const findNearestComponent = (element, componentName) => {
let target = element;
while (target && target.tagName !== "BODY") {
if (target.__vue__ && target.__vue__.$options.name === componentName) {
return target.__vue__;
}
target = target.parentNode;
}
return null;
};

View File

@@ -1,4 +0,0 @@
const initModel = () => {
}
export default initModel

View File

@@ -1 +0,0 @@
export const building1 = import('./building/building1')

View File

@@ -10,8 +10,11 @@
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :total="page.total" :current.sync="page.current" :colConfigs="columns"
:size.sync="page.size" border @getList="getTableData" tableSize="mini" height="430px">
<el-table-column slot="chb" width="100px">
:size.sync="page.size" @getList="getTableData" tableSize="mini" :dict="dict" v-bind="$attrs">
<el-table-column slot="chb" width="100px" v-if="!disabled">
<template #header>
<el-checkbox v-if="multiple" v-model="selectAll" @change="handleCheckAll"/>
</template>
<template slot-scope="{row}">
<el-checkbox v-model="row.checked" @change="handleCheck(row)"/>
</template>
@@ -37,18 +40,23 @@ export default {
default: () => [
{prop: 'label', label: "应用名称"},
{prop: 'project', label: "项目/框架"},
{prop: 'category', label: "分类", dict: "appsCategory"},
{prop: 'name', label: "模块名"},
]
},
nodeKey: {default: "id"},
searchKey: {default: "name"},
multiple: Boolean
multiple: Boolean,
disabled: Boolean,
meta: {default: () => []},
choose: {default: null}
},
data() {
return {
page: {total: 0, current: 1, size: 10},
search: {},
tableData: [],
selectAll: false
}
},
computed: {
@@ -58,7 +66,9 @@ export default {
]
},
selected() {
return [this.value].flat().filter(e => !!e) || []
const {choose, value, nodeKey} = this,
list = [choose].flat().map(e => e?.[nodeKey])
return [...new Set([value, list].flat())].filter(Boolean) || []
}
},
watch: {
@@ -68,40 +78,66 @@ export default {
},
methods: {
getTableData() {
let {page, search, action, nodeKey} = this
this.instance?.post(action, null, {
const {page, search, action, meta, searchKey, dict} = this
if (meta.length > 0) {
const reg = new RegExp(search[searchKey])
this.handleTableData(meta.filter(e => reg.test(e.label) || reg.test(e.name) || reg.test(dict.getLabel('appsCategory', e.category))))
} else this.instance?.post(action, null, {
params: {...page, ...search}
}).then(res => {
if (res?.data) {
const list = res.data.records || res.data || []
list.map(e => {
if (/\/project\//.test(e.libPath)) {
e.project = e.libPath.replace(/.*project\/([^\/]+)\/.+/, '$1')
} else if (/\/core\//.test(e.libPath)) {
e.project = "core"
} else e.project = "standard"
})
this.tableData = list.map(e => ({...e, checked: this.selected.includes(e[nodeKey])}))
this.page.total = res.data.total
this.handleTableData(res.data.records || res.data || [])
}
})
},
handleTableData(list) {
const {nodeKey} = this
list.map(e => {
e.category = e.libPath.replace(/^\/[^\/]+\/([^\/]+)\/.+/, '$1')
if (/\/project\//.test(e.libPath)) {
e.project = e.libPath.replace(/.*project\/([^\/]+)\/.+/, '$1')
} else if (/\/core\//.test(e.libPath)) {
e.project = "core"
} else e.project = "standard"
})
this.tableData = list.map(e => ({...e, checked: this.selected.includes(e[nodeKey])}))
},
handleCheck(row) {
const {nodeKey} = this
if (this.multiple) {
let selected = this.$copy(this.selected)
let selected = this.$copy(this.selected),
choose = this.$copy(this.choose) || []
if (row.checked) {
selected.push(row[nodeKey])
choose.push(row)
} else {
selected = selected.filter(e => e != row[nodeKey])
choose = choose.filter(e => e[nodeKey] != row[nodeKey])
}
this.$emit("change", selected)
this.$emit("update:choose", choose)
this.$emit("select", choose)
} else {
this.tableData.map(e => e.checked = e[nodeKey] == row.id && row.checked)
this.$emit("change", row.checked ? row[nodeKey] : '')
this.$emit("update:choose", row.checked ? row : null)
this.$emit("select", row.checked ? row : null)
}
},
handleCheckAll(v) {
const {nodeKey} = this
let selected = this.tableData.map(e => {
e.checked = v
return e
}).filter(e => e.checked) || []
this.$emit("change", selected?.map(e => e[nodeKey]))
this.$emit("update:choose", selected)
this.$emit("select", selected)
}
},
created() {
this.dict.load('appsCategory')
this.$set(this.search, this.searchKey, "")
this.getTableData()
}

View File

@@ -11,7 +11,7 @@ import Add from "./add";
export default {
name: "AppDeployCustom",
components: {Add, List},
label: "定制项目",
label: "定制方案",
props: {
instance: Function,
dict: Object,
@@ -24,7 +24,7 @@ export default {
}
},
created() {
this.dict.load('yesOrNo','systemType')
this.dict.load('yesOrNo', 'systemType', 'appsCategory')
}
}
</script>

View File

@@ -3,40 +3,87 @@
<ai-detail>
<ai-title slot="title" :title="pageTitle" isShowBottomBorder/>
<template #content>
<el-form ref="AddForm" :model="form" size="small" label-width="120px" :rules="rules">
<ai-card title="基本信息">
<template #content>
<el-form-item label="项目/系统名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" clearable/>
</el-form-item>
<el-row type="flex">
<div class="fill">
<el-form-item label="系统类型" prop="type">
<ai-select v-model="form.type" :selectList="dict.getDict('systemType')"/>
<el-tabs tab-position="left">
<el-tab-pane label="方案设置">
<el-form ref="AddForm" :model="form" size="small" label-width="120px" :rules="rules">
<ai-card title="基本信息">
<template #content>
<el-form-item label="项目/系统名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" clearable/>
</el-form-item>
<el-form-item label="更新项目路径" prop="dist">
<el-input v-model="form.dist" placeholder="常填写nginx路径,下载包从这里取" clearable/>
</el-form-item>
</div>
<div class="fill mar-l16">
<el-form-item label="项目路径" prop="customPath">
<el-input v-model="form.customPath" placeholder="请输入" clearable/>
</el-form-item>
<el-form-item label="版本号" prop="version">
<el-input v-model="form.version" placeholder="请输入" clearable/>
</el-form-item>
</div>
</el-row>
</template>
</ai-card>
<ai-card title="主库应用">
<template #content>
<ai-lib-table v-if="form.type" v-model="form.apps" v-bind="$props" multiple searchKey="name"
:action="`/node/wechatapps/list?type=${form.type}&isMain=1`"/>
<ai-empty v-else>请先选择系统类型</ai-empty>
</template>
</ai-card>
</el-form>
<el-row type="flex">
<div class="fill">
<el-form-item label="系统类型" prop="type">
<ai-select v-model="form.type" :selectList="dict.getDict('systemType')" @change="form.apps = [],handleSysTypeChange(form.type)"/>
</el-form-item>
<el-form-item label="更新项目路径" prop="dist">
<el-input v-model="form.dist" placeholder="常填写nginx路径,下载包从这里取" clearable/>
</el-form-item>
</div>
<div class="fill mar-l16">
<el-form-item label="库项目根路径" prop="customPath">
<el-input v-model="form.customPath" placeholder="请输入" clearable/>
</el-form-item>
<el-form-item label="版本号" prop="version">
<el-input v-model="form.version" placeholder="请输入" clearable/>
</el-form-item>
</div>
</el-row>
</template>
</ai-card>
<ai-card title="主库应用">
<template #content>
<ai-lib-table v-if="form.type" v-model="form.apps" v-bind="$props" multiple searchKey="name"
:action="`/node/wechatapps/list?type=${form.type}&isMain=1`" border/>
<ai-empty v-else>请先选择系统类型</ai-empty>
</template>
</ai-card>
<ai-card title="扩展设置">
<template #content>
<template v-if="form.type=='mp'">
<el-form-item label="小程序AppId">
<el-input v-model="form.appId" clearable placeholder="小程序appId"/>
</el-form-item>
<ai-title title="底部导航栏"/>
<ai-table :tableData="tabBar.list" :colConfigs="colConfigs" tableSize="mini" :isShowPagination="false" border>
<el-table-column slot="options" label="操作" width="80" align="center">
<template slot-scope="{row}">
<ai-dialog-btn text="更换" dialogTitle="选择应用">
<ai-lib-table :meta="appList" v-model="row.id" @select="v=>handleTabbarChange(row,v)" :isShowPagination="false" v-bind="$props"
:border="false"/>
</ai-dialog-btn>
</template>
</el-table-column>
</ai-table>
</template>
<template v-else-if="form.type=='wxwork'">
<el-row type="flex">
<div class="fill">
<el-form-item label="接口是否单服务">
<el-checkbox v-model="form.isSingleService"/>
</el-form-item>
<el-form-item label="是否启用水印">
<el-checkbox v-model="form.waterMarker"/>
</el-form-item>
</div>
<div class="fill">
<el-form-item label="默认首页">
<el-input v-model="form.homePage" clearable placeholder="填写应用的文件名"/>
</el-form-item>
<el-form-item label="开启百度流量">
<el-checkbox v-model="form.hmt"/>
</el-form-item>
</div>
</el-row>
</template>
</template>
</ai-card>
</el-form>
</el-tab-pane>
<el-tab-pane label="方案应用" lazy>
<ai-lib-table :meta="appList" :isShowPagination="false" v-bind="$props" disabled :colConfigs="appListConfigs"/>
</el-tab-pane>
</el-tabs>
</template>
<template #footer>
<el-button @click="back">取消</el-button>
@@ -59,17 +106,53 @@ export default {
},
computed: {
isEdit: v => !!v.$route.query.id,
pageTitle: v => v.isEdit ? "编辑定制项目" : "新增定制项目",
pageTitle: v => v.isEdit ? "编辑定制方案" : "新增定制方案",
appList() {
return this.form.appList?.map(e => {
e.category = e.libPath.replace(/^\/[^\/]+\/([^\/]+)\/.+/, '$1')
if (/\/project\//.test(e.libPath)) {
e.project = e.libPath.replace(/.*project\/([^\/]+)\/.+/, '$1')
} else if (/\/core\//.test(e.libPath)) {
e.project = "core"
} else e.project = "standard"
return e
}) || []
},
},
data() {
return {
form: {apps: []},
form: {apps: [], type: null},
rules: {
name: {required: true, message: "请输入"},
type: {required: true, message: "请选择"},
customPath: {required: true, message: "请输入"},
},
mainLibApps: [],
colConfigs: [
{prop: 'text', label: "名称"},
{prop: 'pagePath', label: "应用路径"},
{prop: 'iconPath', label: "默认图标"},
{prop: 'selectedIconPath', label: "选中图标"},
],
appListConfigs: [
{prop: 'label', label: "应用名称", render: (h, {row}) => h(row.tabbar ? 'b' : 'p', row.label + ` ${row.tabbar ? '(底部导航栏)' : ''}`)},
{prop: 'project', label: "项目/框架"},
{prop: 'category', label: "分类", dict: "appsCategory"},
{prop: 'name', label: "模块名"}
],
tabBar: {
color: "#666666",
selectedColor: "#197DF0",
backgroundColor: "#ffffff",
list: [
{pagePath: "pages/AppHome/AppHome", text: "首页", iconPath: "static/TabBar/home.png", selectedIconPath: "static/TabBar/home_selected.png"},
{pagePath: "pages/AppModules/AppModules", text: "应用", iconPath: "static/TabBar/service.png", selectedIconPath: "static/TabBar/service_selected.png"},
{
pagePath: "pages/AppEnteringVillage/AppEnteringVillage", text: "进村",
iconPath: "static/TabBar/custom.png", selectedIconPath: "static/TabBar/custom_selected.png"
},
{pagePath: "pages/AppMine/AppMine", text: "我的", iconPath: "static/TabBar/me.png", selectedIconPath: "static/TabBar/me_selected.png"}
]
}
}
},
methods: {
@@ -80,6 +163,7 @@ export default {
}).then(res => {
if (res?.data) {
this.form = res.data
this.handleSysTypeChange(this.form.type, this.form.extra)
}
})
},
@@ -89,6 +173,12 @@ export default {
submit() {
this.$refs.AddForm.validate(v => {
if (v) {
const {tabBar, form: {type, appId, isSingleService, homePage}} = this
if (type == 'mp') {
this.form.extra = {tabBar, appId}
} else if (type == 'wxwork') {
this.form.extra = {isSingleService, homePage}
}
this.instance.post("/node/custom/addOrUpdate", this.form).then(res => {
if (res?.code == 0) {
this.$message.success("提交成功!")
@@ -98,6 +188,20 @@ export default {
}
})
},
handleSysTypeChange(v, data = {}) {
let values = this.$copy(data)
if (v == 'mp') {
if (values?.tabBar) {
this.tabBar = values.tabBar || this.tabBar
delete values.tabBar
}
}
Object.keys(values).map(e => this.$set(this.form, e, values[e]))
},
handleTabbarChange(row, {name, label}) {
row.text = label
row.pagePath = `pages/${name}/${name}`
}
},
created() {
this.getDetail()

View File

@@ -1,7 +1,7 @@
<template>
<section class="list">
<ai-list>
<ai-title slot="title" title="定制项目" isShowBottomBorder/>
<ai-title slot="title" title="定制方案" isShowBottomBorder/>
<template #content>
<ai-search-bar>
<template #left>
@@ -19,13 +19,15 @@
<el-progress v-else :percentage="row.count"/>
</template>
</el-table-column>
<el-table-column slot="options" label="操作" fixed="right" align="center" width="300">
<el-table-column slot="options" label="操作" fixed="right" width="300" header-align="center">
<template slot-scope="{row}">
<el-button type="text" @click="handleAdd(row.id)">编辑</el-button>
<el-button type="text" @click="handleUpdate(row)" v-if="row.count==0">打包更新</el-button>
<el-button type="text" @click="handleCancelUpdate(row)" v-else>停止</el-button>
<el-button type="text" @click="handleDownload(row)" v-if="row.dist">下载</el-button>
<el-button type="text" @click="handleDelete(row.id)">删除</el-button>
<template v-if="!!row.dist">
<el-button type="text" @click="handleUpdate(row)" v-if="row.count==0">打包更新</el-button>
<el-button type="text" @click="handleCancelUpdate(row)" v-else>停止</el-button>
<el-button type="text" @click="handleDownload(row)" v-if="row.dist">下载</el-button>
</template>
</template>
</el-table-column>
</ai-table>
@@ -118,7 +120,7 @@ export default {
this.$message.error("打包失败!")
} else if (row.count % 2 == 0 && row.target) {
row.count++
} else this.getRowById(row).then(v => {
} else this.getRowById(row.id).then(v => {
if (v.error) {
clearInterval(timer[id])
this.$message.error("打包失败!")
@@ -152,7 +154,7 @@ export default {
if (res?.code == 0) {
clearInterval(this.timer[id])
row.count = 0
this.getRowById(row).then(v => this.refreshRow(row, v))
this.getRowById(row.id).then(v => this.refreshRow(row, v))
}
})
},

View File

@@ -31,7 +31,8 @@
<el-table-column slot="options" label="操作" fixed="right" align="center" width="300">
<template slot-scope="{row}">
<el-button type="text" @click="handleEdit(row)">编辑</el-button>
<el-button type="text" @click="handleZip(row)">打包</el-button>
<el-button type="text" @click="handleZip(row)" v-if="row.count==0">打包</el-button>
<el-button type="text" @click="handleCancelZip(row)" v-else>停止</el-button>
<el-button type="text" v-if="/^打包时间/.test(row.error)" @click="handleDownload(row)">下载</el-button>
<el-button v-if="row.target" type="text" @click="handleUpdateSystem(row)">更新部署</el-button>
</template>
@@ -44,15 +45,15 @@
<el-form-item label="项目/系统" prop="name">
{{ form.name }}(appid:<b v-text="form.miniapp_appid"/>)
</el-form-item>
<el-form-item label="版本号" prop="version">
<ai-select v-model="form.version" :instance="instance" action="/node/custom/list?type=mp" :prop="{label:'name'}"/>
</el-form-item>
<el-form-item label="小程序上传私钥" prop="privateKey">
<el-input v-model="form.privateKey" clearable placeholder="请输入"/>
</el-form-item>
<el-form-item label="项目地址" prop="projectPath">
<el-input v-model="form.projectPath" clearable placeholder="请输入"/>
</el-form-item>
<el-form-item label="版本号" prop="version">
<el-input v-model="form.version" clearable placeholder="请输入"/>
</el-form-item>
<el-form-item label="npm构建脚本" prop="npmScript">
<el-input v-model="form.npmScript" clearable placeholder="请输入"/>
</el-form-item>
@@ -77,6 +78,7 @@ export default {
desConfigs() {
let isLine = true
return [
{prop: "corp_id", label: "企业微信corpId"},
{prop: "corp_address_book_secret", label: "企业微信通讯录SECRET", width: 200},
{prop: "corp_agent_id", label: "企业微信AGENTID", width: 150},
{prop: "corp_secret", label: "企业微信SECRET", isLine},
@@ -100,10 +102,9 @@ export default {
colConfigs: [
{slot: "expand"},
{label: "项目/系统名称", prop: "name", width: 300},
{label: "corpId", prop: "corp_id", width: 180},
{label: "管理后台", prop: "web_url"},
{label: "appId", prop: "miniapp_appid", width: 180},
{label: "上传版本", prop: "version"},
{label: "管理后台", prop: "web_url"},
{label: "上传版本", render: (h, {row}) => h('p', row.versionName || row.version)},
{slot: "process"},
{slot: "options"}
],
@@ -112,8 +113,9 @@ export default {
rules: {
// privateKey: {required: true, message: "请输入 小程序上传私钥"},
// projectPath: {required: true, message: "请输入 项目地址"},
version: {required: true, message: "请输入 版本号"},
}
version: {required: true, message: "请选择 定制方案"},
},
timer: {}
}
},
methods: {
@@ -155,15 +157,15 @@ export default {
}).then(res => {
if (res?.code == 0) {
row.count = 1
let timer = setInterval(() => {
this.timer[id] = setInterval(() => {
if (row.count >= 100) {
clearInterval(timer)
clearInterval(this.timer[id])
this.$message.error("打包失败!")
} else if (row.count <= 25) {
row.count++
} else this.handleConfirmZip(row).then(v => {
if (v.error) {
clearInterval(timer)
clearInterval(this.timer[id])
row.error = v.error
row.count = 0
} else row.count++
@@ -172,6 +174,10 @@ export default {
}
}).catch(() => this.$message.error("打包失败!"))
},
handleCancelZip(row) {
clearInterval(this.timer[row.id])
row.count = 0
},
handleUpdateSystem(row) {
let {appid} = row
return this.instance.post("/node/wxmp/updateSystem", null, {
@@ -225,6 +231,9 @@ export default {
},
created() {
this.getTableData()
},
beforeDestroy() {
Object.values(this.timer).map(t => clearInterval(t))
}
}
</script>

View File

@@ -0,0 +1,4 @@
{
"AppResident": "居民档案",
"AppResidentTags": "标签管理"
}

View File

@@ -0,0 +1,79 @@
<template>
<div class="AppAnnounce">
<!-- <keep-alive :include="['List']"> -->
<component ref="component" :is="component" @change="onChange" :params="params" :instance="instance" :dict="dict"></component>
<!-- </keep-alive> -->
</div>
</template>
<script>
import List from './components/List'
import Add from './components/Add'
import Detail from './components/Detail'
export default {
name: 'AppAnnounce',
label: '群发居民群',
props: {
instance: Function,
dict: Object
},
data () {
return {
component: 'List',
params: {},
include: []
}
},
components: {
Add,
List,
Detail
},
mounted () {
if (this.$route.params.id) {
this.component = 'Detail'
this.params = {
id: this.$route.params.id
}
}
},
methods: {
onChange (data) {
if (data.type === 'Add') {
this.component = 'Add'
this.params = data.params
}
if (data.type === 'Detail') {
this.component = 'Detail'
this.params = data.params
}
if (data.type === 'list') {
this.component = 'List'
this.params = data.params
this.$nextTick(() => {
if (data.isRefresh) {
this.$refs.component.getList()
}
})
}
}
}
}
</script>
<style lang="scss">
.AppAnnounce {
height: 100%;
background: #F3F6F9;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,924 @@
<template>
<ai-detail class="AppAnnounceAdd">
<template slot="title">
<ai-title :title="id ? '编辑居民群发' : '添加居民群发'" isShowBack isShowBottomBorder @onBackClick="cancel(false)">
</ai-title>
</template>
<template slot="content">
<div class="AppAnnounceDetail-container">
<el-form ref="form" class="left" :model="form" label-width="110px" label-position="right">
<ai-card title="基本信息">
<template #content>
<div class="ai-form">
<el-form-item label="任务名称" prop="taskTitle" style="width: 100%;" :rules="[{ required: true, message: '请输入任务名称', trigger: 'blur' }]">
<el-input size="small" placeholder="请输入任务名称" v-model="form.taskTitle" :maxlength="15" show-word-limit></el-input>
</el-form-item>
<el-form-item label="发送范围" style="width: 100%;" prop="sendScope" :rules="[{ required: true, message: '请选择发送范围', trigger: 'change' }]">
<el-radio-group v-model="form.sendScope" @change="onScopeChange">
<el-radio label="0">全部居民群</el-radio>
<el-radio label="1">按部门选择</el-radio>
<el-radio label="2">按网格选择</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择群主" v-if="form.sendScope !== '0'" prop="wxGroupsName" style="width: 100%;" :rules="[{ required: true, message: '请选择选择群主', trigger: 'change' }]">
<ai-picker
:instance="instance"
multiple
:dialogTitle="form.sendScope === '2' ? '选择网格' : '选择部门'"
:ops="{label: form.sendScope === '2' ? 'girdName' : 'name'}"
:pageTitle="form.sendScope === '2' ? '网格' : '部门'"
:action="form.sendScope === '1' ? '/app/wxcp/wxdepartment/departList' : '/app/appgirdinfo/girdList'"
v-model="form.filterCriteria"
@pick="onPick"
@change="onSelcetChange">
<div class="AppAnnounceDetail-select">
<el-input size="small" class="AppAnnounceDetail-select__input" placeholder="请选择..." disabled v-model="form.wxGroupsName"></el-input>
<div class="select-left" v-if="form.wxGroups.length">
<span v-for="(item, index) in form.wxGroups" :key="index" v-if="index < 9">
<ai-open-data
type="userName"
:openid="item.groupOwnerId">
</ai-open-data>
</span>
<em v-if="form.wxGroups.length > 9">{{ form.wxGroups.length }}</em>
</div>
<i v-if="!form.wxGroups.length">请选择</i>
<div class="select-right">{{ form.filterCriteria.length ? '重新选择' : '选择' }}</div>
</div>
</ai-picker>
<div class="tips">
<p>消息预计送达居民群数</p>
<span>{{ groupLen }}</span>
<el-tooltip
placement="top"
content="将由指定群主发送给TA作为群主的所有的群由于企业微信限制当超过1000个时将只发送到最近活跃的1000个群">
<i class="iconfont iconModal_Warning"></i>
</el-tooltip>
</div>
</el-form-item>
<el-form-item label="发送内容" prop="content" style="width: 100%;" :rules="[{ required: true, message: '请输入发送内容', trigger: 'blur' }]">
<el-input size="small" type="textarea" :rows="6" maxlength="1300" show-word-limit placeholder="请输入文本内容..." v-model="form.content"></el-input>
<div class="add">
<div class="fileList" v-if="fileList.length">
<div class="add-item" v-for="(item, index) in fileList" :key="index">
<div class="left">
<img :src="mapIcon(item.msgType)"/>
<span>{{ item.mpTitle || item.name || item.linkTitle }}</span>
</div>
<i @click="removeFile(index)">删除</i>
</div>
</div>
<el-popover
placement="top"
width="340"
offset="0"
trigger="hover">
<div class="add-item" slot="reference" style="width: max-content;">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/add.png"/>
<span style="color: #2266FF; font-size: 12px;">添加附件类型</span>
</div>
<div class="AppAnnounceDetail-content-wrapper">
<el-upload
ref="upload"
multiple
:file-list="fileList"
:show-file-list="false"
:before-upload="v => handleChange(v, 10, '.jpg,.png,.jpeg')"
:limit="9"
action="/app/wxcp/upload/uploadFile"
accept=".jpg,.png,.jpeg"
:on-exceed="onExceed"
:http-request="v => submitUpload(v, '1')">
<div class="content-item" trigger>
<img src="https://cdn.cunwuyun.cn/dvcp/announce/big-img.png"/>
<p>图片</p>
</div>
</el-upload>
<el-upload
ref="upload"
multiple
:file-list="fileList"
:show-file-list="false"
:before-upload="v => handleChange(v, 10, '.mp4')"
:limit="9"
action="/app/wxcp/upload/uploadFile"
accept=".mp4"
:on-exceed="onExceed"
:http-request="v => submitUpload(v, '2')">
<div class="content-item" trigger>
<img src="https://cdn.cunwuyun.cn/dvcp/announce/big-video.png"/>
<p>视频</p>
</div>
</el-upload>
<el-upload
ref="upload"
multiple
:file-list="fileList"
:show-file-list="false"
:before-upload="v => handleChange(v, 20, '.zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt')"
:limit="9"
:on-exceed="onExceed"
action="/app/wxcp/upload/uploadFile"
accept=".zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt"
:http-request="v => submitUpload(v, '3')">
<div class="content-item" trigger>
<img src="https://cdn.cunwuyun.cn/dvcp/announce/folder.png"/>
<p>文件</p>
</div>
</el-upload>
<div class="content-item" @click="isShowAddLink = true">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/site.png"/>
<p>网页</p>
</div>
<div class="content-item" @click="isShowAddMiniapp = true">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/miniapp.png"/>
<p>小程序</p>
</div>
</div>
</el-popover>
</div>
<div class="tips">
<em>从本地上传图片最大支持10MB支持JPG,PNG格式视频最大支持10MB支持MP4格式文件最大支持20MB</em>
</div>
</el-form-item>
<el-form-item label="宣发审批" prop="enableExamine" style="width: 100%;" :rules="[{ required: true, message: '请输入任务名称', trigger: 'blur' }]">
<el-switch
v-model="form.enableExamine"
active-value="1"
inactive-value="0"
active-text="开启后创建的群发任务需要审批人进行审批">
</el-switch>
</el-form-item>
<el-form-item v-if="form.enableExamine === '1'" label="审批人员" prop="examines" style="width: 100%;" :rules="[{ required: true, message: '请选择审批人员', trigger: 'change' }]">
<ai-user-get :instance="instance" v-model="form.examines" @change="onUserChange">
<div class="AppAnnounceDetail-select">
<el-input class="AppAnnounceDetail-select__input" size="small" placeholder="请选择..." v-model="form.examinesName"></el-input>
<div class="select-left" v-if="form.examines.length">
<span v-for="(item, index) in form.examines" :key="index">
<ai-open-data type="userName" :openid="item.wxOpenUserId"></ai-open-data>
</span>
</div>
<i v-if="!form.examines.length">请选择</i>
<div class="select-right">{{ form.examines.length ? '重新选择' : '选择' }}</div>
</div>
</ai-user-get>
</el-form-item>
</div>
</template>
</ai-card>
</el-form>
<div class="right">
<Phone :avatar="user.info.avatar" @close="isShowPhone = false" :isShowClose="false" :content="form.content" :fileList="fileList"></Phone>
</div>
<ai-dialog
:visible.sync="isShowAddLink"
width="920px"
title="链接消息"
@close="onClose"
@onConfirm="onLinkConfirm">
<el-form ref="linkForm" :model="linkForm" label-width="110px" label-position="right">
<div class="ai-form">
<el-form-item label="标题" style="width: 100%;" prop="linkTitle" :rules="[{ required: true, message: '请输入标题', trigger: 'blur' }]">
<el-input
size="small"
placeholder="请输入标题"
maxlength="42"
show-word-limit
v-model="linkForm.linkTitle">
</el-input>
</el-form-item>
<el-form-item label="链接" style="width: 100%;" prop="linkUrl" :rules="[{ required: true, message: '请输入链接', trigger: 'blur' }]">
<el-input
size="small"
placeholder="请输入链接"
maxlength="682"
show-word-limit
v-model="linkForm.linkUrl">
</el-input>
</el-form-item>
<el-form-item label="描述" style="width: 100%;" prop="linkDesc">
<el-input
size="small"
placeholder="请输入描述"
maxlength="170"
show-word-limit
v-model="linkForm.linkDesc">
</el-input>
</el-form-item>
<el-form-item label="封面图" prop="linkPicUrl" style="width: 100%;">
<ai-uploader :instance="instance" v-model="linkForm.linkPicUrl" :limit="1"></ai-uploader>
</el-form-item>
</div>
</el-form>
</ai-dialog>
<ai-dialog
:visible.sync="isShowAddMiniapp"
width="920px"
title="小程序消息"
@close="onClose"
@onConfirm="onMiniAppForm">
<el-form ref="miniAppForm" :model="miniAppForm" label-width="130px" label-position="right">
<div class="ai-form">
<el-form-item label="小程序appid" style="width: 100%;" prop="mpAppid" :rules="[{ required: true, message: '小程序appid', trigger: 'blur' }]">
<el-input
size="small"
placeholder="小程序appid"
v-model="miniAppForm.mpAppid">
</el-input>
</el-form-item>
<el-form-item label="小程序page路径" style="width: 100%;" prop="mpPage" :rules="[{ required: true, message: '请输入小程序page路径', trigger: 'blur' }]">
<el-input
size="small"
placeholder="请输入小程序page路径"
v-model="miniAppForm.mpPage">
</el-input>
</el-form-item>
<el-form-item label="标题" style="width: 100%;" prop="mpTitle" :rules="[{ required: true, message: '请输入标题', trigger: 'blur' }]">
<el-input
size="small"
placeholder="请输入标题"
maxlength="20"
show-word-limit
v-model="miniAppForm.mpTitle">
</el-input>
</el-form-item>
<el-form-item label="封面图" prop="media" style="width: 100%;" :rules="[{ required: true, message: '请上传封面图', trigger: 'change' }]">
<ai-uploader url="/app/wxcp/upload/uploadFile?type=image" :instance="instance" isWechat v-model="miniAppForm.media" :limit="1"></ai-uploader>
</el-form-item>
</div>
</el-form>
</ai-dialog>
<ai-dialog
:visible.sync="isShowDate"
width="590px"
title="定时发送"
customFooter>
<el-form ref="dateForm" :model="dateForm" label-width="130px" label-position="right">
<div class="ai-form">
<el-form-item label="定时发送时间" style="width: 100%;" prop="choiceTime" :rules="[{ required: true, message: '请选择定时发送时间', trigger: 'change' }]">
<el-date-picker
style="width: 100%;"
v-model="dateForm.choiceTime"
type="datetime"
size="small"
:picker-options="pickerOptions"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择定时发送时间">
</el-date-picker>
</el-form-item>
</div>
</el-form>
<div class="dialog-footer" slot="footer">
<el-button @click="onClose">取消</el-button>
<el-button @click="onDateForm" type="primary" :loading="isLoading2" style="width: 92px;">确认</el-button>
</div>
</ai-dialog>
</div>
</template>
<template #footer>
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="confirm(0)" :loading="isLoading1" style="width: 120px;">通知成员发送</el-button>
<el-button type="primary" @click="confirm(1)">定时发送</el-button>
</template>
</ai-detail>
</template>
<script>
import Phone from './Phone'
import {mapActions, mapState} from 'vuex'
export default {
name: 'Add',
props: {
instance: Function,
dict: Object,
params: Object
},
components: {
Phone
},
data() {
return {
info: {},
department: [],
isLoading1: false,
isLoading2: false,
fileList: [],
isShowAddLink: false,
isShowAddMiniapp: false,
isShowDate: false,
isLoading: false,
linkForm: {
linkPicUrl: [],
linkDesc: '',
linkTitle: '',
linkUrl: ''
},
dateForm: {
choiceTime: ''
},
miniAppForm: {
mpAppid: '',
mpPage: '',
mpTitle: '',
media: []
},
form: {
content: '',
choiceTime: '',
contents: [],
enableExamine: '0',
examines: [],
wxGroups: [],
wxGroupsName: '',
sendScope: '0',
sendType: 0,
name: '',
filterCriteria: [],
taskTitle: '',
examinesName: ''
},
girdNames: '',
id: '',
tagsList: [],
pickerOptions: {
disabledDate: e => {
return e.getTime() < (Date.now() - 60 * 1000 * 60 * 24)
}
}
}
},
computed: {
...mapState(['user']),
groupLen() {
let i = 0
this.form.wxGroups.forEach(v => {
i = i + v.groupIds.split(',').length
})
return i
}
},
created() {
if (this.params && this.params.id) {
this.id = this.params.id
this.getInfo(this.params.id)
} else {
this.getWxGroups()
}
},
methods: {
...mapActions(['initOpenData', 'transCanvas']),
getInfo(id) {
this.instance.post(`/app/appmasssendingtask/queryDetailById?id=${id}`).then(res => {
if (res.code === 0) {
this.form = {
...this.form,
...res.data,
wxGroupsName: '1',
filterCriteria: res.data.filterCriteria.split(',')
}
if (res.data.girdNames) {
this.girdNames = res.data.girdNames.split(',')
}
this.dateForm.choiceTime = ''
if (res.data.examines && res.data.examines.length) {
this.form.examines = res.data.examines.map(v => {
return {
...v,
wxOpenUserId: v.examineUserId,
id: v.examineUserId
}
})
this.form.examinesName = '1'
}
const content = res.data.contents.filter(v => v.msgType === '0')
if (content.length) {
this.$set(this.form, 'content', content[0].content)
}
this.fileList = res.data.contents.filter(v => v.msgType !== '0').map(v => {
return {
...v,
...v.sysFile
}
})
}
})
},
onUserChange(e) {
if (e.length) {
this.form.examinesName = '1'
} else {
this.form.wxGroupsName = ''
}
},
onScopeChange(e) {
this.form.filterCriteria = []
this.form.wxGroups = []
this.girdNames = ''
if (e === '0') {
this.getWxGroups()
} else {
this.form.filterCriteria = []
}
},
onPick(e) {
if (this.form.sendScope === '2' && e.length) {
this.girdNames = e.map(v => v.girdName)
}
},
onSelcetChange(e) {
if (e.length) {
this.form.wxGroupsName = '1'
this.$nextTick(() => {
this.getWxGroups()
})
} else {
this.form.wxGroupsName = ''
this.form.wxGroups = []
}
},
getWxGroups() {
this.instance.post(`/app/appmasssendingtask/queryWxGroups?sendScope=${this.form.sendScope}`, null, {
data: {
filterCriteria: this.form.filterCriteria.join(',')
},
headers: {'Content-Type': 'application/json;charset=utf-8'},
transformRequest: [function (data) {
return data.filterCriteria
}]
}).then(res => {
if (res.code === 0) {
this.form.wxGroups = res.data
}
})
},
onLinkConfirm() {
this.$refs.linkForm.validate((valid) => {
if (valid) {
this.fileList.push({
...this.linkForm,
linkPicUrl: this.linkForm.linkPicUrl.length ? this.linkForm.linkPicUrl[0].url : '',
msgType: '4'
})
this.isShowAddLink = false
}
})
},
onMiniAppForm() {
this.$refs.miniAppForm.validate((valid) => {
if (valid) {
this.fileList.push({
...this.miniAppForm,
msgType: '5',
...this.miniAppForm.media[0],
mediaId: this.miniAppForm.media[0].media.mediaId,
sysFileId: this.miniAppForm.media[0].id
})
this.isShowAddMiniapp = false
}
})
},
onClose() {
this.linkForm.linkPicUrl = []
this.linkForm.linkDesc = ''
this.linkForm.linkTitle = ''
this.linkForm.linkUrl = ''
this.miniAppForm.mpAppid = ''
this.miniAppForm.mpPage = ''
this.miniAppForm.mpTitle = ''
this.dateForm.choiceTime = ''
this.isShowDate = false
},
removeFile(index) {
this.fileList.splice(index, 1)
},
mapIcon(type) {
return {
1: 'https://cdn.cunwuyun.cn/dvcp/announce/img.png',
2: 'https://cdn.cunwuyun.cn/dvcp/announce/video.png',
3: 'https://cdn.cunwuyun.cn/dvcp/announce/folder.png',
4: 'https://cdn.cunwuyun.cn/dvcp/announce/site.png',
5: 'https://cdn.cunwuyun.cn/dvcp/announce/miniapp.png'
}[type]
},
onBeforeUpload(event) {
return this.onOverSize(event)
},
getExtension(name) {
return name.substring(name.lastIndexOf('.'))
},
handleChange(e, size, accept) {
const isLt10M = e.size / 1024 / 1024 < size
const suffixName = this.getExtension(e.name)
const suffixNameList = accept.split(',')
if (suffixNameList.indexOf(`${suffixName.toLowerCase()}`) === -1) {
this.$message.error(`不支持该格式`)
return false
}
if (!isLt10M) {
this.$message.error(`大小不超过${10}MB!`)
return false
}
return true
},
onExceed() {
this.$message.error(`最多上传9个附件`)
},
submitUpload(file, type) {
const fileType = {
'1': 'image',
'2': 'video',
'3': 'file'
}[type]
let formData = new FormData()
formData.append('file', file.file)
formData.append('type', fileType)
let loading = this.$loading()
this.instance.post(`/app/wxcp/upload/uploadFile`, formData, {
withCredentials: false
}).then(res => {
if (res.code == 0) {
this.fileList.push({
...res.data.file,
media: res.data.media,
msgType: type,
sysFileId: res.data.file.id,
imgPicUrl: res.data.file.url,
mediaId: res.data.media.mediaId
})
this.$message.success('上传成功')
}
}).finally(() => loading.close())
},
onDateForm() {
this.$refs.dateForm.validate((valid) => {
if (valid) {
if (new Date(this.dateForm.choiceTime).getTime() < Date.now()) {
return this.$message.error('定时发送时间不得早于当前时间')
} else {
this.confirm(1)
}
}
})
},
confirm(sendType) {
this.$refs.form.validate((valid) => {
if (valid) {
if (!this.form.wxGroups.length) {
return this.$message.error('居民群数量不能为0')
}
if (sendType === 1 && !this.dateForm.choiceTime) {
this.isShowDate = true
return false
}
const contents = [
{
content: this.form.content,
msgType: '0'
},
...this.fileList
]
if (sendType === 0) {
this.isLoading1 = true
} else {
this.isLoading2 = true
}
this.instance.post(`/app/appmasssendingtask/addOrUpdate`, {
...this.form,
id: this.params.id,
wxGroups: this.form.wxGroups,
contents,
sendType,
choiceTime: this.dateForm.choiceTime,
filterCriteria: this.form.filterCriteria.join(','),
examines: this.form.examines.length ? this.form.examines.map(v => {
return {
...v,
examineUserId: v.id
}
}) : []
}).then(res => {
if (res.code == 0) {
this.$message.success('提交成功')
setTimeout(() => {
this.cancel(true)
}, 600)
} else {
this.isLoading1 = false
this.isLoading2 = false
}
}).catch(() => {
this.isLoading1 = false
this.isLoading2 = false
})
}
})
},
cancel(isRefresh) {
this.$emit('change', {
type: 'list',
isRefresh: !!isRefresh
})
}
}
}
</script>
<style lang="scss">
.el-tooltip__popper.is-dark {
max-width: 240px;
}
.AppAnnounceDetail-content-wrapper {
display: flex;
align-items: center;
.content-item {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 64px;
height: 64px;
line-height: 1;
margin-right: 4px;
text-align: center;
background: #F9F9F9;
border-radius: 2px;
cursor: pointer;
&:hover {
opacity: 0.6;
}
&:last-child {
margin-right: 0;
}
img {
width: 32px;
height: 32px;
margin-bottom: 4px;
}
p {
color: #222;
font-size: 12px;
}
}
}
.AppAnnounceAdd {
.ai-detail__content {
.ai-detail__content--wrapper {
position: relative;
max-width: 100%;
margin: 0;
height: 100%;
overflow: hidden;
}
}
.ai-form {
textarea {
border-radius: 4px 4px 0 0!important;
}
}
* {
box-sizing: border-box;
}
.add {
display: flex;
flex-direction: column;
padding: 14px 16px;
background: #F9F9F9;
border-radius: 0px 0px 4px 4px;
border: 1px solid #D0D4DC;
border-top: none;
.add-item {
display: flex;
align-items: center;
line-height: 1;
cursor: pointer;
&:hover {
opacity: 0.6;
}
img {
width: 20px;
height: 20px;
margin-right: 2px;
}
span {
color: #222;
font-size: 14px;
}
}
.fileList {
margin-bottom: 12px;
.add-item {
justify-content: space-between;
margin-bottom: 8px;
.left {
display: flex;
align-items: center;
}
i {
font-size: 14px;
cursor: pointer;
font-style: normal;
color: red;
&:hover {
opacity: 0.6;
}
}
&:last-child {
margin-bottom: 0;
}
}
}
}
.AppAnnounceDetail-container {
display: flex;
position: relative;
height: 100%;
padding: 0 20px;
overflow: auto;
overflow: overlay;
.left {
flex: 1;
margin-right: 20px;
}
.right {
position: sticky;
top: 0;
}
.AppAnnounceDetail-select {
display: flex;
align-items: center;
min-height: 32px;
line-height: 1;
background: #F5F5F5;
border-radius: 4px;
border: 1px solid #D0D4DC;
cursor: pointer;
overflow: hidden;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #26f;
}
& > i {
flex: 1;
height: 100%;
line-height: 32px;
padding: 0 12px;
color: #888888;
font-size: 14px;
font-style: normal;
border-right: 1px solid #D0D4DC;
background: #fff;
}
.AppAnnounceDetail-select__input {
position: absolute;
left: 0;
top: 0;
z-index: -1;
opacity: 0;
height: 100%;
}
.select-right {
height: 100%;
padding: 0 12px;
color: #222222;
font-size: 12px;
cursor: pointer;
transition: all ease 0.3s;
&:hover {
opacity: 0.5;
}
}
.select-left {
display: flex;
flex-wrap: wrap;
flex: 1;
padding: 5px 0 0px 12px;
border-right: 1px solid #D0D4DC;
border-radius: 4px 0 0 4px;
background: #fff;
em {
height: 22px;
line-height: 22px;
margin: 0 4px 5px 0;
color: #222222;
font-size: 12px;
font-style: normal;
}
span {
height: 22px;
line-height: 22px;
margin: 0 4px 5px 0;
padding: 0 8px;
font-size: 12px;
color: #222222;
background: #F3F4F7;
border-radius: 2px;
border: 1px solid #D0D4DC;
}
}
}
}
.tips {
display: flex;
align-items: center;
font-size: 14px;
color: #222222;
span {
margin: 0 3px;
color: #2266FF;
}
i {
color: #8899bb;
}
em {
line-height: 20px;
margin-top: 8px;
color: #888888;
font-size: 12px;
font-style: normal;
}
}
}
</style>

View File

@@ -0,0 +1,777 @@
<template>
<ai-detail class="AppAnnounceDetail">
<template slot="title">
<ai-title title="群发详情" isShowBack isShowBottomBorder @onBackClick="cancel(false)">
</ai-title>
</template>
<template slot="content">
<ai-card title="基础信息">
<template #right>
<div class="right-tips" v-if="info.status === '4'">
<el-tooltip
placement="top"
content="任务开始后3天内15分钟更新1次3天后访问页面时触发更新1小时最多刷新1次">
<i class="iconfont iconDetails"></i>
</el-tooltip>
<span>数据更新于{{ info.dataUpdateTime }}</span>
</div>
</template>
<template #content>
<ai-wrapper>
<ai-info-item label="任务名称" isLine :value="info.taskTitle"></ai-info-item>
<ai-info-item label="任务状态" isLine>
<span :style="{ color: dict.getColor('mstStatus', info.status) }">{{ dict.getLabel('mstStatus', info.status) }}</span>
</ai-info-item>
<ai-info-item label="创建人" isLine>
<div class="user">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/user.png" />
<span><ai-open-data type="userName" :openid="info.createUserId"></ai-open-data></span>
<span><ai-open-data type="departmentName" :openid="info.createUserDept"></ai-open-data></span>
</div>
</ai-info-item>
<ai-info-item label="审批人" isLine v-if="info.enableExamine === '1'">
<div class="user-wrapper">
<div class="user" v-for="(item, index) in info.examines" :key="index">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/user.png" />
<span><ai-open-data type="userName" :openid="item.examineUserId"></ai-open-data></span>
</div>
</div>
</ai-info-item>
<ai-info-item label="创建时间" :value="info.createTime"></ai-info-item>
<ai-info-item label="群发时间" :value="info.choiceTime"></ai-info-item>
<ai-info-item label="群发范围" isLine>
<div class="text">
<span>{{ info.sendScope === '0' ? '全部' : '按条件筛选的' }}</span>
<i>{{ groups.length }}</i>
<span>个居民群</span>
<em @click="isShowGroups = true">详情</em>
</div>
</ai-info-item>
<ai-info-item label="消息内容" isLine>
<div class="msg">
<p>{{ content }}</p>
<div class="msg-bottom">
<div class="left" v-if="fileList.length">
<img :src="mapIcon(fileList[0].msgType)" />
<span>{{ mapType(fileList[0].msgType) }}{{ fileList[0].mpTitle || fileList[0].name || fileList[0].linkTitle }} </span>
<i>{{ fileList.length }}</i>
<span>个附件</span>
</div>
<div class="left" v-else>
<span>暂无附件</span>
</div>
<div class="right" @click="isShowPhone = true">预览消息</div>
</div>
</div>
</ai-info-item>
</ai-wrapper>
</template>
</ai-card>
<ai-card>
<template #title>
<div class="AppAnnounceDetail-title">
<span :class="[currIndex === 0 ? 'active' : '']" @click="currIndex = 0">成员统计</span>
<span :class="[currIndex === 1 ? 'active' : '']" @click="currIndex = 1">居民群统计</span>
</div>
</template>
<template #content>
<div class="content-item" v-if="currIndex === 0">
<div class="top">
<div class="top-item">
<div class="top-item__title">
<h3>计划执行成员</h3>
</div>
<p>{{ memberInfo.planCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>未执行成员</h3>
</div>
<p>{{ memberInfo.unExecutedCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>已执行成员</h3>
</div>
<p>{{ memberInfo.executedCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>无法执行成员</h3>
<el-tooltip
placement="top"
content="由于员工不在可见范围、离职、客户群接收已达到上限等原因,无法执行群发任务的成员总数">
<i class="iconfont iconDetails"></i>
</el-tooltip>
</div>
<p>{{ memberInfo.cannotExecuteCount || 0 }}</p>
</div>
</div>
<div class="bottom">
<div class="bottom-search">
<div class="left">
<el-radio-group v-model="search1.sendStatus" size="small" @change="search1.current = 1, getMemberInfo()">
<el-radio-button size="small" label="0">未执行</el-radio-button>
<el-radio-button size="small" label="1">已执行</el-radio-button>
<el-radio-button size="small" label="2">无法执行</el-radio-button>
</el-radio-group>
<ai-picker
dialogTitle="选择部门"
action="/app/wxcp/wxdepartment/departList"
:instance="instance"
@pick="e => onUserChange(e, 'search1')" :multiple="false" v-model="user1">
<div class="userSelcet">
<span style="color: #606266;" v-if="search1.deptartId"><ai-open-data type="departmentName" :openid="search1.deptartId"></ai-open-data></span>
<span v-else>部门</span>
<i class="el-icon-arrow-up" v-if="!search1.deptartId"></i>
<i class="el-icon-circle-close" v-if="search1.deptartId" @click.stop="user1 = [], search1.deptartId = '', search1.current = 1, getMemberInfo()"></i>
</div>
</ai-picker>
</div>
<el-button :type="isDisabled ? '' : 'primary'" :disabled="isDisabled" @click="sendMsg(0)" v-if="info.status === '4'">{{ isDisabled ? min + '分钟后可再次提醒' : '提醒成员发送' }}</el-button>
</div>
<ai-table
:tableData="tableData1"
:col-configs="colConfigs1"
:total="total1"
border
tableSize="small"
:current.sync="search1.current"
:size.sync="search1.size"
@getList="getMemberInfo">
<el-table-column slot="user" label="成员" align="left">
<template slot-scope="{ row }">
<div class="userinfo">
<span>
<ai-open-data type="userName" :openid="row.groupOwnerId"></ai-open-data>
</span>
<span style="color: #999">
<ai-open-data type="departmentName" :openid="row.mainDepartment"></ai-open-data>
</span>
</div>
</template>
</el-table-column>
</ai-table>
</div>
</div>
<div class="content-item" v-if="currIndex === 1">
<div class="top">
<div class="top-item">
<div class="top-item__title">
<h3>计划送达居民群</h3>
</div>
<p>{{ groupInfo.planCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>未送达居民群</h3>
</div>
<p>{{ groupInfo.unExecutedCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>已送达居民群</h3>
</div>
<p>{{ groupInfo.executedCount || 0 }}</p>
</div>
<div class="top-item">
<div class="top-item__title">
<h3>无法送达居民群</h3>
</div>
<p>{{ groupInfo.cannotExecuteCount || 0 }}</p>
</div>
</div>
<div class="bottom">
<div class="bottom-search">
<div class="left">
<el-radio-group v-model="search2.sendStatus" size="small" @change="search2.current = 1, getGroupInfo()">
<el-radio-button size="small" label="0">未送达</el-radio-button>
<el-radio-button size="small" label="1">已送达</el-radio-button>
<el-radio-button size="small" label="2">无法送达</el-radio-button>
</el-radio-group>
<ai-picker
dialogTitle="选择部门"
action="/app/wxcp/wxdepartment/departList"
:instance="instance"
@pick="e => onUserChange(e, 'search2')" :multiple="false" v-model="user2">
<div class="userSelcet">
<span style="color: #606266;" v-if="search2.deptartId"><ai-open-data type="departmentName" :openid="search2.deptartId"></ai-open-data></span>
<span v-else>部门</span>
<i class="el-icon-arrow-up" v-if="!search2.deptartId"></i>
<i class="el-icon-circle-close" v-if="search2.deptartId" @click.stop="user1 = [], search2.deptartId = '', search2.current = 1, getGroupInfo()"></i>
</div>
</ai-picker>
</div>
<el-button :type="isDisabled ? '' : 'primary'" :disabled="isDisabled" @click="sendMsg(1)" v-if="info.status === '4'">{{ isDisabled ? min + '分钟后可再次提醒' : '提醒成员发送' }}</el-button>
</div>
<ai-table
:tableData="tableData2"
:col-configs="colConfigs2"
:total="total2"
border
tableSize="small"
:current.sync="search2.current"
:size.sync="search2.size"
@getList="getGroupInfo">
<el-table-column slot="user" label="群主" align="center">
<template slot-scope="{ row }">
<div class="userinfo">
<span>
<ai-open-data type="userName" :openid="row.groupOwnerId"></ai-open-data>
</span>
<span style="color: #999">
<ai-open-data type="departmentName" :openid="row.mainDepartment"></ai-open-data>
</span>
</div>
</template>
</el-table-column>
</ai-table>
</div>
</div>
</template>
</ai-card>
<ai-dialog
:visible.sync="isShowGroups"
width="890px"
title="群发范围"
@onConfirm="isShowGroups = false">
<ai-table
:tableData="info.wxGroups"
:col-configs="colConfigs3"
border
tableSize="small"
:isShowPagination="false"
@getList="() => {}">
</ai-table>
</ai-dialog>
<div class="detail-phone" v-if="isShowPhone">
<div class="mask"></div>
<Phone :avatar="user.info.avatar" @close="isShowPhone = false" :isShowClose="true" :content="content" :fileList="fileList"></Phone>
</div>
</template>
</ai-detail>
</template>
<script>
import { mapState } from 'vuex'
import Phone from './Phone'
export default {
name: 'Detail',
props: {
instance: Function,
dict: Object,
params: Object
},
components: {
Phone
},
data () {
return {
total1: 0,
isShowGroups: false,
isShowPhone: false,
total2: 0,
user1: [],
user2: [],
radio1: '未执行',
search1: {
current: 1,
size: 10,
deptartId: '',
type: 0,
sendStatus: '0'
},
search2: {
current: 1,
size: 10,
deptartId: '',
type: 1,
sendStatus: '0'
},
memberInfo: {},
groupInfo: {},
tableData1: [],
fileList: [],
tableData2: [],
info: {},
content: '',
currIndex: 0,
colConfigs3: [
{ prop: 'groupOwnerId', label: '群主', openType: 'userName' },
{ prop: 'groupNames', label: '群名称' }
],
colConfigs1: [
{ slot: 'user', label: '成员', openType: 'userName' },
{ prop: 'groupCount', label: '预计送达居民群', align: 'center' }
],
colConfigs2: [
{ prop: 'groupName', label: '居民群' },
{ prop: 'memberCount', label: '群人数', align: 'center' },
{ slot: 'user', label: '群主', align: 'center' },
],
groups: [],
timer: null,
min: 60,
isDisabled: false,
rejecterId: ''
}
},
computed: {
...mapState(['user'])
},
created () {
this.getInfo(this.params.id)
this.getMemberInfo()
this.getGroupInfo()
},
destroyed () {
clearInterval(this.timer)
},
methods: {
getMemberInfo () {
this.instance.post(`/app/appmasssendingtask/detailStatistics`, null, {
params: {
...this.search1,
taskId: this.params.id
}
}).then(res => {
if (res.code === 0) {
this.tableData1 = res.data.executedList.records
this.total1 = res.data.executedList.total
this.memberInfo = res.data
}
})
},
onUserChange (e, search) {
if (e.length) {
this[search].deptartId = e[0].id
} else {
this[search].deptartId = ''
}
this[search].current = 1
if (search === 'search1') {
this.getMemberInfo()
} else {
this.getGroupInfo()
}
},
sendMsg () {
this.instance.post(`/app/appmasssendingtask/remindSend?id=${this.params.id}`).then(res => {
if (res.code === 0) {
this.$message.success('提醒成功')
this.getInfo(this.params.id)
}
})
},
getGroupInfo () {
this.instance.post(`/app/appmasssendingtask/detailStatistics`, null, {
params: {
...this.search2,
taskId: this.params.id
}
}).then(res => {
if (res.code === 0) {
this.tableData2 = res.data.executedList.records.map(v => {
return {
...v,
groupName: v.groupName || '未命名群聊'
}
})
this.total2 = res.data.executedList.total
this.groupInfo = res.data
}
})
},
countdown () {
this.timer = setInterval(() => {
const nowTime = this.$moment(new Date())
const min = nowTime.diff(this.info.remindTime, 'minute')
this.min = (60 - min)
if (this.min <= 0) {
this.isDisabled = false
clearInterval(this.timer)
} else {
this.isDisabled = true
}
}, 1000)
},
getInfo (id) {
this.instance.post(`/app/appmasssendingtask/queryDetailById?id=${id}`).then(res => {
if (res.code === 0) {
this.info = res.data
if (res.data.status === '4' && res.data.remindTime) {
this.countdown()
}
const content = res.data.contents.filter(v => v.msgType === '0')
if (content.length) {
this.content = content[0].content
}
this.fileList = res.data.contents.filter(v => v.msgType !== '0').map(v => {
return {
...v,
...v.sysFile
}
})
this.info.wxGroups = res.data.wxGroups.map(v => {
this.groups.push(...v.groupIds.split(','))
return {
...v,
groupIds: v.groupIds.split(',')
}
})
if (res.data.examines && res.data.examines.length) {
const user = res.data.examines.filter(v => v.examineStatus === '2')
if (user.length) {
this.rejecterId = user[0].examineUserId
}
}
}
})
},
mapType (type) {
return {
1: '图片',
2: '视频',
3: '文件',
4: '网站',
5: '小程序'
}[type]
},
mapIcon (type) {
return {
1: 'https://cdn.cunwuyun.cn/dvcp/announce/img.png',
2: 'https://cdn.cunwuyun.cn/dvcp/announce/video.png',
3: 'https://cdn.cunwuyun.cn/dvcp/announce/folder.png',
4: 'https://cdn.cunwuyun.cn/dvcp/announce/site.png',
5: 'https://cdn.cunwuyun.cn/dvcp/announce/miniapp.png'
}[type]
},
cancel (isRefresh) {
this.$emit('change', {
type: 'list',
isRefresh: !!isRefresh
})
}
}
}
</script>
<style scoped lang="scss">
.AppAnnounceDetail {
position: relative;
.user-wrapper {
display: flex;
flex-wrap: wrap;
}
.detail-phone {
position: fixed;
left: 0%;
top: 0%;
z-index: 11;
width: 100%;
height: 100%;
.mask {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 1;
background: rgba($color: #000000, $alpha: 0.6);
}
::v-deep .phone-container {
position: absolute;
left: 50%;
top: 50%;
z-index: 11;
transform: translate(-50%, -50%);
}
}
.userSelcet {
display: flex;
align-items: center;
justify-content: space-between;
width: 215px;
height: 32px;
line-height: 32px;
margin-left: 12px;
border-radius: 4px;
border: 1px solid #d0d4dc;
overflow: hidden;
cursor: pointer;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #26f;
}
i {
display: flex;
position: relative;
align-items: center;
justify-content: center;
width: 30px;
height: 100%;
line-height: 32px;
font-size: 14px;
text-align: center;
color: #d0d4dc;
transform: rotateZ(180deg);
}
.el-icon-circle-close:hover {
opacity: 0.6;
}
span {
flex: 1;
padding: 0 15px;
font-size: 12px;
color: $placeholderColor;
}
}
.userinfo {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1;
span:first-child {
margin-bottom: 4px;
}
}
.user {
display: flex;
align-items: center;
line-height: 1;
margin-right: 8px;
img {
width: 16px;
height: 16px;
margin-right: 2px;
}
span {
position: relative;
top: 2px;
color: #222222;
font-size: 12px;
}
}
.text {
display: flex;
align-items: center;
i {
color: #2266FF;
font-style: normal;
}
em {
margin-left: 8px;
color: #2266FF;
font-size: 12px;
font-style: normal;
cursor: pointer;
transition: all ease 0.3s;
&:hover {
opacity: 0.6;
}
}
}
.msg {
background: #F9F9F9;
border-radius: 2px;
border: 1px solid #D0D4DC;
p {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
line-height: 38px;
padding: 0px 12px;
overflow: hidden;
}
.msg-bottom {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
padding: 0 16px;
border-top: 1px solid #D0D4DC;
.left {
display: flex;
align-items: center;
img {
width: 16px;
height: 16px;
margin-right: 8px;
}
span {
color: #222222;
font-size: 14px;
}
i {
color: #2266FF;
font-size: 14px;
font-style: normal;
}
}
.right {
color: #2266FF;
font-size: 12px;
cursor: pointer;
&:hover {
opacity: 0.6;
}
}
}
}
::v-deep .AppAnnounceDetail-title {
display: flex;
align-items: center;
span {
height: 100%;
line-height: 56px;
margin-right: 32px;
color: #888888;
font-size: 16px;
font-weight: 600;
transition: all ease 0.3s;
border-bottom: 3px solid transparent;
cursor: pointer;
user-select: none;
&:hover {
color: #222;
}
&:last-child {
margin-right: 0;
}
&.active {
color: #222222;
border-bottom: 3px solid #2266FF;
}
}
}
.content-item {
.top {
display: flex;
align-items: center;
margin-bottom: 16px;
.top-item {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
height: 90px;
margin-right: 16px;
padding: 0 16px;
background: #F9F9F9;
border-radius: 2px;
&:last-child {
margin-right: 0;
}
.top-item__title {
display: flex;
align-items: center;
margin-bottom: 8px;
i {
margin-left: 4px;
color: #8899bb;
font-size: 16px;
}
}
h3 {
color: #222222;
font-size: 14px;
font-weight: 700;
}
p {
color: #2266FF;
font-size: 24px;
font-weight: 700;
}
}
}
.bottom-search {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.left {
display: flex;
align-items: center;
}
}
}
::v-deep .right-tips {
display: flex;
align-items: center;
i {
margin-right: 4px;
color: #8899bb;
font-size: 16px;
}
span {
color: #888888;
font-size: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<ai-list class="AppAnnounce">
<template slot="title">
<ai-title title="群发居民群" isShowBottomBorder>
<template #sub>
<span>管理员统一创建宣发任务选择要发送的居民群后通知群主发送群主确认后即可群发到居民群群主向同一个居民群每天最多可群发10条消息</span>
</template>
</ai-title>
</template>
<template slot="content">
<ai-search-bar class="search-bar">
<template #left>
<el-button size="small" type="primary" icon="iconfont iconAdd" @click="toAdd('')">创建宣发</el-button>
<ai-select
v-model="search.status"
@change="search.current = 1, getList()"
placeholder="任务状态"
:selectList="dict.getDict('mstStatus')">
</ai-select>
<el-date-picker
v-model="search.startTime"
type="date"
size="small"
value-format="yyyy-MM-dd"
@change="search.current = 1, getList()"
placeholder="选择群发开始日期">
</el-date-picker>
<el-date-picker
v-model="search.endTime"
type="date"
size="small"
value-format="yyyy-MM-dd"
@change="search.current = 1, getList()"
placeholder="选择群发结束日期">
</el-date-picker>
<ai-user-get :instance="instance" @change="onUserChange" :isMultiple="false" v-model="user">
<div class="userSelcet">
<span style="color: #606266;" v-if="search.createUserId"><ai-open-data type="userName" :openid="search.createUserId"></ai-open-data></span>
<span v-else>创建人</span>
<i class="el-icon-arrow-up" v-if="!search.createUserId"></i>
<i class="el-icon-circle-close" v-if="search.createUserId" @click.stop="user = [], search.createUserId = '', search.current = 1, getList()"></i>
</div>
</ai-user-get>
</template>
<template slot="right">
<el-input
v-model="search.taskTitle"
size="small"
v-throttle="() => { search.current = 1, getList() }"
placeholder="请输入任务名称"
clearable
@clear="search.current = 1, search.taskTitle = '', getList()"
suffix-icon="iconfont iconSearch">
</el-input>
</template>
</ai-search-bar>
<ai-table
:tableData="tableData"
:col-configs="colConfigs"
:total="total"
v-loading="loading"
style="margin-top: 6px; width: 100%;"
:current.sync="search.current"
:size.sync="search.size"
@getList="getList">
<el-table-column slot="user" width="140px" label="创建人" align="center">
<template slot-scope="{ row }">
<div class="userinfo">
<span>
<ai-open-data type="userName" :openid="row.createUserId"></ai-open-data>
</span>
<span style="color: #999">
<ai-open-data type="departmentName" :openid="row.createUserDept"></ai-open-data>
</span>
</div>
</template>
</el-table-column>
<el-table-column slot="options" width="140px" fixed="right" label="操作" align="center">
<template slot-scope="{ row }">
<div class="table-options">
<el-button type="text" @click="remindExamine(row.id)" v-if="['0'].includes(row.status)">催办</el-button>
<el-button type="text" @click="cancel(row.id)" v-if="['0'].includes(row.status)">撤回</el-button>
<el-button type="text" @click="toDetail(row.id)">详情</el-button>
<el-button type="text" @click="toAdd(row.id)" v-if="['1', '3'].includes(row.status)">编辑</el-button>
</div>
</template>
</el-table-column>
</ai-table>
</template>
</ai-list>
</template>
<script>
export default {
name: 'List',
props: {
instance: Function,
dict: Object
},
data() {
return {
search: {
current: 1,
size: 10,
status: '',
createUserId: '',
taskTitle: '',
startTime: '',
endTime: ''
},
user: [],
tableData: [],
loading: false,
total: 0,
colConfigs: [
{ prop: 'taskTitle', label: '任务名称' },
{ prop: 'typeName', label: '群发类型', align: 'center' },
{ slot: 'user', label: '创建人', openType: 'userName', align: 'center' },
{ prop: 'choiceTime', label: '群发时间', align: 'center' },
{
prop: 'status',
align: 'center',
label: '状态',
render: (h, {row}) => {
return h('span', {
style: {
color: this.dict.getColor('mstStatus', row.status)
}
}, this.dict.getLabel('mstStatus', row.status))
}
},
{ prop: 'completionRate', label: '任务完成率', align: 'center', formart: v => v ? v === '0.0' ? '0%' : `${v}%` : '-' }
]
}
},
created () {
this.dict.load('mstStatus', 'mstSendType').then(() => {
this.getList()
})
},
methods: {
onUserChange (e) {
if (e.length) {
this.search.createUserId = e[0].wxOpenUserId
} else {
this.search.createUserId = ''
}
this.search.current = 1
this.getList()
},
getList() {
this.loading = true
this.instance.post(`/app/appmasssendingtask/list`, null, {
params: {
...this.search,
}
}).then(res => {
if (res.code == 0) {
this.tableData = res.data.records.map(v => {
return {
...v,
typeName: '群发居民群'
}
})
this.total = res.data.total
this.$nextTick(() => {
this.loading = false
})
} else {
this.loading = false
}
}).catch(() => {
this.loading = false
})
},
remindExamine (id) {
this.$confirm('确认再次通知任务审核人员?').then(() => {
this.instance.post(`/app/appmasssendingtask/remindExamine?id=${id}`).then(res => {
if (res.code == 0) {
this.$message.success('催办成功!')
this.getList()
}
})
})
},
cancel (id) {
this.$confirm('确认撤回该群发任务?').then(() => {
this.instance.post(`/app/appmasssendingtask/cancel?id=${id}`).then(res => {
if (res.code == 0) {
this.$message.success('撤回成功!')
this.getList()
}
})
})
},
remove(id) {
this.$confirm('确定删除该数据?').then(() => {
this.instance.post(`/app/appmasssendingtask/delete?ids=${id}`).then(res => {
if (res.code == 0) {
this.$message.success('删除成功!')
this.getList()
}
})
})
},
toAdd(id) {
this.$emit('change', {
type: 'Add',
params: {
id
}
})
},
toDetail (id) {
this.$emit('change', {
type: 'Detail',
params: {
id
}
})
}
}
}
</script>
<style lang="scss" scoped>
.AppAnnounce {
height: 100%;
.userinfo {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
line-height: 1;
span:first-child {
margin-bottom: 4px;
}
}
.userSelcet {
display: flex;
align-items: center;
justify-content: space-between;
width: 215px;
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #d0d4dc;
overflow: hidden;
cursor: pointer;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #26f;
}
i {
display: flex;
position: relative;
align-items: center;
justify-content: center;
width: 30px;
height: 100%;
line-height: 32px;
font-size: 14px;
text-align: center;
color: #d0d4dc;
transform: rotateZ(180deg);
}
.el-icon-circle-close:hover {
opacity: 0.6;
}
span {
flex: 1;
padding: 0 15px;
font-size: 12px;
color: $placeholderColor;
}
}
}
</style>

View File

@@ -0,0 +1,344 @@
<template>
<div class="phone-container">
<img class="close" @click="$emit('close')" v-if="isShowClose" src="https://cdn.cunwuyun.cn/dvcp/announce/close.png" />
<img class="phone" src="https://cdn.cunwuyun.cn/dvcp/announce/phone.png" />
<img class="phone-wrapper" src="https://cdn.cunwuyun.cn/dvcp/announce/phone-wrapper.png" />
<div class="right-content">
<div class="msg-list">
<div class="msg-item" v-if="content">
<div class="msg-item__left">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/avatar.png" />
</div>
<div class="msg-item__right">
<div class="msg-wrapper msg-text">
<p>{{ content }}</p>
</div>
</div>
</div>
<div class="msg-item" v-for="item in fileList" :key="item.id">
<div class="msg-item__left">
<img src="https://cdn.cunwuyun.cn/dvcp/announce/avatar.png" />
</div>
<div class="msg-item__right" :class="[['1', '2'].indexOf(item.msgType) !== -1 ? 'left-border' : '']">
<div class="msg-wrapper msg-img" v-if="item.msgType === '1'">
<img :src="item.imgPicUrl" />
</div>
<div class="msg-wrapper msg-video" v-if="item.msgType === '2'">
<video controls :src="item.url"></video>
</div>
<div class="msg-wrapper msg-file" v-if="item.msgType === '3'">
<div class="msg-left">
<h2>{{ item.name }}</h2>
<p>{{ item.fileSizeStr }}</p>
</div>
<img :src="mapIcon(item.name)" />
</div>
<div class="msg-wrapper msg-link" v-if="item.msgType === '4'">
<h2>{{ item.linkTitle }}</h2>
<div class="msg-right">
<p>{{ item.linkDesc }}</p>
<img :src="item.linkPicUrl || 'https://cdn.cunwuyun.cn/dvcp/announce/html.png'" />
</div>
</div>
<div class="msg-wrapper msg-miniapp" v-if="item.msgType === '5'">
<h2>{{ item.mpTitle }}</h2>
<img :src="item.url" />
<div class="msg-bottom">
<i>小程序</i>
<img src="https://cdn.cunwuyun.cn/dvcp/announce/miniapp.png">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['fileList', 'avatar', 'content', 'isShowClose'],
watch: {
fileList (v) {
if (v.length) {
setTimeout(() => {
document.querySelector('.right-content').scrollTo(0, 999999)
}, 800)
}
}
},
methods: {
mapIcon (fileName) {
if (['.zip', '.rar'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/zip.png'
}
if (['.doc', '.docx'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/world.png'
}
if (['.xls', '.xlsx'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/xls.png'
}
if (['.txt'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/txt.png'
}
if (['.pdf'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/pdf.png'
}
if (['.ppt', '.pptx'].indexOf(this.getExtension(fileName)) !== -1) {
return 'https://cdn.cunwuyun.cn/dvcp/announce/ppt.png'
}
},
getExtension(name) {
return name.substring(name.lastIndexOf('.'))
}
}
}
</script>
<style lang="scss" scoped>
.phone-container {
width: 338px;
height: 675px;
padding: 80px 15px 100px 32px;
.phone {
position: absolute;
left: 13px;
top: 4px;
z-index: 1;
width: 314px;
height: 647px;
}
.close {
position: absolute;
top: 0;
right: 0;
z-index: 111;
width: 60px;
height: 60px;
cursor: pointer;
transition: all ease 0.5s;
transform: translate(100%, -50%);
&:hover {
opacity: 0.7;
}
}
.phone-wrapper {
position: absolute;
left: 0;
top: 0;
z-index: 2;
width: 338px;
height: 675px;
}
.right-content {
position: relative;
z-index: 11;
height: 100%;
overflow-y: auto;
.msg-item {
display: flex;
margin-bottom: 20px;
.msg-item__left {
width: 42px;
height: 42px;
margin-right: 16px;
border-radius: 4px;
flex-shrink: 1;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
.msg-item__right {
position: relative;
flex: 1;
&::after {
position: absolute;
top: 16px;
left: 0;
z-index: 1;
width: 0;
height: 0;
border-right: 6px solid #fff;
border-left: 6px solid transparent;
border-bottom: 6px solid transparent;
border-top: 6px solid transparent;
content: " ";
transform: translate(-100%, 0%);
}
&.left-border::after {
display: none;
}
.msg-img img {
max-width: 206px;
max-height: 200px;
}
.msg-video video {
max-width: 206px;
max-height: 200px;
}
.msg-text {
max-width: 206px;
width: max-content;
line-height: 1.3;
padding: 12px;
background: #FFFFFF;
border-radius: 5px;
word-break: break-all;
font-size: 14px;
color: #222222;
}
.msg-miniapp {
width: 206px;
padding: 0 12px;
text-align: justify;
font-size: 0;
background: #FFFFFF;
border-radius: 5px;
font-size: 14px;
color: #222222;
h2 {
line-height: 1.2;
padding: 8px 0;
border-bottom: 1px solid #eee;
color: #222222;
font-size: 14px;
}
& > img {
width: 100%;
height: 120px;
margin-bottom: 8px;
}
.msg-bottom {
display: flex;
align-items: center;
line-height: 1;
padding: 4px 0;
border-top: 1px solid #eee;
i {
margin-right: 4px;
font-size: 12px;
font-style: normal;
color: #999;
}
img {
width: 16px;
height: 16px;
border-radius: 50%;
}
}
}
.msg-file {
display: flex;
align-items: center;
width: 206px;
padding: 12px;
background: #FFFFFF;
border-radius: 5px;
.msg-left {
flex: 1;
margin-right: 18px;
h2 {
display: -webkit-box;
flex: 1;
line-height: 16px;
margin-bottom: 4px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
text-overflow: ellipsis;
overflow: hidden;
color: #222222;
font-size: 14px;
width: 120px;
}
p {
color: #888888;
font-size: 12px;
}
}
img {
width: 44px;
height: 44px;
border-radius: 2px;
}
}
.msg-link {
width: 206px;
padding: 12px;
background: #FFFFFF;
border-radius: 5px;
h2 {
margin-bottom: 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #222222;
font-size: 14px;
font-weight: normal;
}
.msg-right {
display: flex;
align-items: center;
p {
display: -webkit-box;
flex: 1;
line-height: 16px;
margin-right: 10px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
text-overflow: ellipsis;
overflow: hidden;
color: #888;
font-size: 12px;
}
img {
width: 50px;
height: 50px;
border-radius: 4px;
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,759 @@
<template>
<ai-list class="AppAnnounceStatistics">
<template slot="content">
<div class="statistics-content">
<ai-title title="宣发日历"></ai-title>
<div class="flex-content">
<div class="flex-left">
<div class="date-header">
<p>{{chooseYear}}{{chooseMonth}}</p>
<div>
<el-date-picker size="small"
v-model="searchMonth"
type="month" value-format="yyyy-MM"
placeholder="选择日期" @change="searchMonthChange">
</el-date-picker>
</div>
</div>
<el-calendar v-model="calendarDate">
<template
slot="dateCell"
slot-scope="{date, data}" >
<div class="flex-date">
<span>{{Number(data.day.substring(8, 10))}}</span>
<span class="tips" v-if="data.day.substring(5, 7) == chooseMonth && dateList[Number(data.day.substring(8, 10))] && dateList[Number(data.day.substring(8, 10))].taskList.length">{{dateList[Number(data.day.substring(8, 10))].taskList.length}}</span>
</div>
</template>
</el-calendar>
</div>
<div class="flex-right">
<div class="title">{{chooseMonth}}{{chooseDay}}日宣发内容</div>
<div class="list-content" v-if="taskList.length">
<el-timeline >
<el-timeline-item v-for="(item, index) in taskList" :key="index">
<el-card>
<div class="flex-between">
<p class="item-title">{{item.taskTitle}}</p>
<span class="item-time" v-if="item.choiceTime">{{item.choiceTime.substring(10, 16)}}</span>
</div>
<div class="item-info item-created">
<span class="label">创建人</span>
<ai-open-data type="userName" :openid="item.createUserId" class="name"></ai-open-data>
</div>
<div class="item-info item-dept">
<span class="label">创建部门</span>
<ai-open-data type="departmentName" :openid="item.createUserDept" class="name"></ai-open-data>
</div>
<div class="flex-between">
<!-- <div class="item-info">群发类型<span>{{$dict.getLabel('mstSendType', item.sendType) || ''}}</span></div> -->
<div class="item-info"><span class="label">群发类型</span><span>群发居民群</span></div>
<span class="item-btn" @click="$router.push({name: '357e228ba8e64008ace90d095a7a0dd7', params: { id: item.id }})">详情</span>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<ai-empty v-if="!taskList.length" />
</div>
</div>
</div>
<div class="statistics-content">
<div class="flex-between mar-b16">
<ai-title title="宣发效果"></ai-title>
<div class="right-search">
<div class="time-select" :class="effectType == index ? 'active' : ''" v-for="(item, index) in dateTypeList" :key="index" @click="changeEffectType(index)">{{item}}</div>
<ai-picker :instance="instance" @pick="e => onUserChange(e)" :multiple="false" dialogTitle="选择部门" action="/app/wxcp/wxdepartment/departList">
<div class="time-select">
<span class="dept-name" style="color:#999;" v-if="deptList && !deptList.length">宣发部门</span>
<ai-open-data class="dept-name" type="departmentName" :openid="deptList[0].id" v-else/>
<i class="el-icon-arrow-down"></i>
</div>
</ai-picker>
</div>
</div>
<div class="line-content">
<div class="flex1">
<div class="header">
<p>累计创建宣发任务数</p>
<h2>{{effectData.createCount}}</h2>
</div>
<div class="chart-content">
<div class="chart-title">宣发任务数</div>
<div class="chart-box" id="createChart"></div>
</div>
</div>
<div class="flex1">
<div class="header">
<p>累计执行宣发次数</p>
<h2>{{effectData.executeCount}}</h2>
</div>
<div class="chart-content">
<div class="chart-title">宣发次数</div>
<div class="chart-box" id="executeChart"></div>
</div>
</div>
<div class="flex1 mar-r0">
<div class="header">
<p>累计触达人次</p>
<h2>{{effectData.receiveCount}}</h2>
</div>
<div class="chart-content">
<div class="chart-title">触达人次</div>
<div class="chart-box" id="receiveChart"></div>
</div>
</div>
</div>
</div>
<div class="statistics-content">
<div class="flex-between mar-b16">
<ai-title title="宣发明细"></ai-title>
<div class="right-search">
<div class="time-select" :class="departType == index ? 'active' : ''" v-for="(item, index) in dateTypeList" :key="index" @click="changeDepartType(index)">{{item}}</div>
</div>
</div>
<div id="departBarChart" v-if="isDepartData"></div>
<ai-empty v-if="!isDepartData"></ai-empty>
</div>
<ai-dialog :visible.sync="dialogDate" title="选择时间" width="500px" customFooter>
<el-date-picker v-model="timeList" size="small" type="daterange" value-format="yyyy-MM-dd"
range-separator="" start-placeholder="开始日期" end-placeholder="结束日期">
</el-date-picker>
<el-button slot="footer" @click="selectDete" type="primary">确认</el-button>
</ai-dialog>
</template>
</ai-list>
</template>
<script>
import * as echarts from "echarts";
import { mapActions, mapState } from 'vuex';
export default {
name: 'AppAnnounceStatistics',
label: '协同宣发统计',
props: {
instance: Function,
dict: Object,
permissions: Function
},
data () {
return {
calendarDate: new Date(),
dateList: {},
chooseYear: '',
chooseMonth: '',
chooseDay: '',
searchMonth: '',
taskList: [],
effectType: 0, // 宣发效果类型 0近七天、1近30天、2近一年、3自定义
effectData: {},
createChart: null,
executeChart: null,
receiveChart: null,
departType: 0, // 宣发明细类型 0近七天、1近30天、2近一年、3自定义
dateTypeList: ['近7天', '近30天', '近1年', '自定义'],
departData: {},
departBarChart: null,
dialogDate: false,
timeListEffect: '',
timeListDepart: '',
timeList: '',
isEffectTimeSelect: false,
deptList: [],
selectDeptName: '',
isDepartData: true,
departBarData: [],
type: '',
}
},
computed: {
...mapState(['user']),
},
watch: {
calendarDate: function() {
var year = '' , month = '', date = ''
if(this.calendarDate.length == 9) { // 月份选择器触发
year = this.calendarDate.substring(0, 4)
month = this.calendarDate.substring(5, 7)
date = this.calendarDate.substring(8, 10)
}else { // 日历点击
year = this.calendarDate.getFullYear();
month = this.calendarDate.getMonth() + 1;
date = this.calendarDate.getDate()
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if(this.chooseMonth != month) { // 日历点击不同月
this.searchMonth = ''
}
}
this.chooseDay = date
if(this.chooseMonth != month || this.chooseYear != year) { // 不同年/不同月重新请求日历列表
this.getCalendarList(year, month)
} else {
this.getTaskList(date)
}
this.chooseMonth = month
this.chooseYear = year
}
},
created() {
var year = this.calendarDate.getFullYear();
var month = this.calendarDate.getMonth() + 1;
var date = this.calendarDate.getDate()
if (month >= 1 && month <= 9) {
month = "0" + month;
}
this.chooseMonth = month
this.chooseYear = year
this.chooseDay = date
this.getCalendarList(year, month)
this.getEffect()
this.getDepart()
this.dict.load('mstSendType')
},
methods: {
...mapActions(['initOpenData', 'transCanvas']),
onUserChange (e) {
this.deptList = e
this.getEffect()
},
selectDete() {
if(!this.timeList || !this.timeList.length) {
return this.$message.error('请选择自定义时间');
}
if(this.isEffectTimeSelect) { //宣发效果
this.timeListEffect = this.timeList
this.effectType = 3
this.getEffect()
} else { //宣发明细
this.timeListDepart = this.timeList
this.departType = 3
this.getDepart()
}
this.dialogDate = false
},
searchMonthChange() {
this.calendarDate = this.searchMonth + '-1'
},
getCalendarList(year, month){
this.instance.post(`/app/appmasssendingtask/statisticsCalendar?yyyyMM=${year}${month}`).then(res => {
if (res.code == 0) {
this.dateList = res.data
this.getTaskList(this.chooseDay)
}
})
},
getTaskList(day) {
this.taskList = this.dateList[day].taskList
},
changeEffectType(type) {
if(this.effectType != 3) {
this.timeList = []
}else {
this.timeList = this.timeListEffect
}
if(type == 3) {
this.isEffectTimeSelect = true
this.dialogDate = true
}else {
this.effectType = type
this.getEffect()
}
},
getEffect() {
var startTime = this.timeListEffect[0] || '' , endTime = this.timeListEffect[1] || '', departId = this.deptList[0] || ''
this.instance.post(`/app/appmasssendingtask/statisticsEffect?type=${this.effectType}&startTime=${startTime}&endTime=${endTime}&departId=${departId}`).then(res => {
if (res.code == 0) {
this.effectData = res.data
var xData = [], createData = [], executeData = [], receiveData = []
res.data.trend.map(e => {
if(this.effectType == 0 || this.effectType == 1) {
e.ymd = e.ymd.substring(5, 10)
}
xData.push(e.ymd)
createData.push(e.createCount)
executeData.push(e.executeCount)
receiveData.push(e.receiveCount)
})
this.setLineChart(xData, createData, 'createChart', ['#2891FF'])
this.setLineChart(xData, executeData, 'executeChart', ['#FFB865'])
this.setLineChart(xData, receiveData, 'receiveChart', ['#26D52B'])
}
})
},
setLineChart(xData, yData, id, colorList) {
this[id] = echarts.init(document.querySelector(`#${id}`))
var option = {
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
grid: {
left: '10px',
right: '28px',
bottom: '14px',
top: '30px',
containLabel: true
},
tooltip: {
trigger: 'axis'
},
legend: {
type: "plain"
},
color: colorList,
series: [
{
data: yData,
type: 'line'
}
]
}
this[id].setOption(option)
},
changeDepartType(type) {
if(this.departType != 3) {
this.timeList = []
}else {
this.timeList = this.timeListDepart
}
if(type == 3) {
this.isEffectTimeSelect = false
this.dialogDate = true
}else {
this.departType = type
this.getDepart()
}
},
getDepart() {
var startTime = this.timeListDepart[0] || '' , endTime = this.timeListDepart[1] || ''
this.instance.post(`/app/appmasssendingtask/statisticsDepart?type=${this.departType}&startTime=${startTime}&endTime=${endTime}`).then(res => {
if (res.code == 0) {
if(res.data && res.data.length) {
this.isDepartData = true
var items = [], xData = [], yData = []
res.data.map((item) => {
this.departBarData.push(item)
var i = {type: 'departmentName', id: item.deptId, corpid: this.user.info.corpId}
items.push(i)
yData.push(item.taskCount)
})
this.initOpenData({canvas:true})
this.transCanvas(items).then((data) => {
xData = data.items.map((i) => {
return i.data
})
this.setBarChart(xData, yData)
})
}else {
this.isDepartData = false
}
}
})
},
setBarChart(xData, yData) {
this.departBarChart = echarts.init(document.querySelector(`#departBarChart`))
var option = {
color: ['#2891FF'],
grid: {
top: '10%',
left: '2%',
right: '2%',
bottom: 90,
containLabel: true
},
// toolbox: {
// feature: {
// dataZoom: {
// yAxisIndex: false
// },
// saveAsImage: {
// pixelRatio: 2
// }
// }
// },
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (data) => {
var index = data[0].dataIndex
return `<ww-open-data type="departmentName" openid="${this.departBarData[index].deptId}"></ww-open-data><br/>宣发任务数:${data[0].value}`
}
},
dataZoom: [
{
type: 'inside'
},
{
type: 'slider'
}
],
xAxis: {
data: xData,
silent: false,
splitLine: {
show: false
},
splitArea: {
show: false
}
},
yAxis: {
splitArea: {
show: false
}
},
series: [
{
type: 'bar',
data: yData,
barWidth: 20,
barGap: '250%',
large: true
}
]
};
// {
// tooltip: {
// trigger: 'axis',
// axisPointer: {
// type: 'shadow'
// }
// },
// grid: {
// top: '10%',
// left: '2%',
// right: '2%',
// bottom: '2%',
// containLabel: true
// },
// color: ['#2891FF'],
// xAxis: {
// type: 'category',
// data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
// },
// yAxis: {
// type: 'value'
// },
// series: [
// {
// data: [120, 200, 150, 80, 70, 110, 130],
// type: 'bar',
// barWidth: 20,
// barGap: '250%',
// }
// ]
// };
this.departBarChart.setOption(option)
}
}
}
</script>
<style lang="scss" scoped>
.AppAnnounceStatistics {
height: 100%;
.flex-between{
display: flex;
justify-content: space-between;
}
.mar-b16{
margin-bottom: 16px;
}
.mar-r0{
margin-right: 0!important;
}
.statistics-content{
padding: 0 24px 24px;
background-color: #fff;
box-shadow: 0px 4px 6px -2px rgba(15,15,21,0.1500);
border-radius: 4px;
margin-bottom: 20px;
.flex-content{
width: 100%;
display: flex;
margin-top: 16px;
.flex-left{
width: 50%;
.date-header{
padding: 12px 16px;
border: 1px solid #eee;
display: flex;
justify-content: space-between;
p{
line-height: 32px;
}
}
.flex-date{
display: flex;
justify-content: space-between;
}
.tips{
display: inline-block;
padding: 0 4px;
height: 16px;
line-height: 16px;
border-radius: 8px;
background: #2891FF;
font-size: 12px;
font-family: ArialMT;
color: #FFF;
margin-top: 8px;
}
}
.flex-right{
width: 50%;
margin-left: 16px;
border: 1px solid #eee;
.title{
line-height: 56px;
border-bottom: 1px solid #EEE;
padding-left: 16px;
font-size: 16px;
font-family: MicrosoftYaHeiSemibold;
color: #333;
}
.list-content{
padding: 16px;
height: 339px;
box-sizing: border-box;
overflow-y: scroll;
background-color: #F9F9F9;
box-sizing: border-box;
.item-title{
width: calc(100% - 100px);
word-break: break-all;
margin-bottom: 8px;
font-size: 16px;
font-family: MicrosoftYaHeiSemibold;
color: #222;
line-height: 24px;
}
.item-time{
width: 100px;
text-align: right;
font-size: 16px;
font-family: ArialMT;
color: #888;
line-height: 24px;
}
.item-info{
display: inline-block;
font-size: 14px;
font-family: MicrosoftYaHei;
color: #222;
line-height: 22px;
span{
display: inline-block;
color: #222;
word-break: break-all;
// vertical-align: text-top;
}
.label{
color: #999;
}
}
.item-created{
width: 152px;
margin-bottom: 4px;
.label{
width: 56px;
}
.name{
width: calc(100% - 56px);
}
}
.item-dept{
width: calc(100% - 152px);
.label{
width: 70px;
}
.name{
width: calc(100% - 70px);
}
}
.item-btn{
color: #26f;
cursor: pointer;
}
}
}
}
.right-search{
margin-top: 10px;
div{
display: inline-block;
}
.time-select{
font-size: 14px;
font-family: MicrosoftYaHei;
color: #222;
line-height: 22px;
padding: 6px 12px;
border-radius: 2px;
border: 1px solid #D0D4DC;
margin-right: 8px;
box-sizing: border-box;
cursor: pointer;
.dept-name{
display: inline-block;
width: 200px;
height: 22px;
overflow:hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: bottom;
}
.el-icon-arrow-down{
vertical-align: middle;
}
}
.active{
border: 1px solid #26f;
color: #26f;
}
}
.line-content{
display: flex;
.flex1{
flex: 1;
margin-right: 16px;
.header{
padding: 16px;
width: 100%;
height: 90px;
background: #F9F9F9;
border-radius: 2px;
box-sizing: border-box;
margin-bottom: 16px;
p{
font-size: 14px;
font-family: MicrosoftYaHeiSemibold;
color: #222;
line-height: 22px;
margin-bottom: 4px;
}
h2{
font-size: 24px;
font-family: DINAlternate-Bold, DINAlternate;
font-weight: bold;
color: #26F;
line-height: 32px;
}
}
.chart-content{
width: 100%;
padding: 16px;
background: #F9F9F9;
border-radius: 2px;
box-sizing: border-box;
.chart-title{
font-size: 16px;
font-family: MicrosoftYaHeiSemibold;
color: #333;
line-height: 24px;
}
.chart-box{
width: 100%;
height: 280px;
}
}
}
}
#departBarChart{
width: 100%;
height: 300px;
}
}
::v-deep .el-calendar-table:not(.is-range) td.next,
::v-deep .el-calendar-table:not(.is-range) td.prev {
color: #ccc;
}
::v-deep .el-calendar-table .el-calendar-day{
height: 48px;
line-height: 32px;
padding-left: 12px;
font-size: 14px;
font-family: ArialMT;
}
.el-calendar-table:not(.is-range) td .current{
color: #888;
}
::v-deep .el-calendar__header{
display: none;
}
::v-deep .el-calendar__body{
padding: 0;
}
::v-deep .el-calendar-table thead th:nth-of-type(1){
border-left: 1px solid #eee;
}
::v-deep .el-calendar-table thead th:nth-of-type(7){
border-right: 1px solid #eee;
}
::v-deep .el-calendar-table tr td:first-child {
border-left: 1px solid #eee;
}
::v-deep .el-calendar-table tr:first-child td {
border-top: 1px solid #eee;
}
::v-deep .el-calendar-table td {
border-bottom: 1px solid #eee;
border-right: 1px solid #eee;
}
::v-deep .el-timeline-item__timestamp.is-top{
margin-bottom: 0;
padding-top: 0;
}
::v-deep .el-timeline-item__node{
background-color: #26F;
width: 8px;
height: 8px;
border-radius: 50%;
left: 1px;
}
::v-deep .el-card{
border: none;
}
::v-deep .el-card__body{
padding: 8px;
}
}
::v-deep .ai-list__content {
padding: 0!important;
.ai-list__content--right-wrapper {
background: transparent!important;
box-shadow: none!important;
margin: 0!important;
padding: 0 0 0!important;
}
}
::v-deep .AiPicker{
display: inline-block;
}
</style>

View File

@@ -574,7 +574,7 @@ export default {
this.$nextTick(() => {
this.loading = false
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
})
} else {
this.loading = false

View File

@@ -167,7 +167,7 @@
</div>
<p>可以将生成的二维码或链接分享给居民</p>
<el-input size="small" :value="info.linkUrl">
<el-button slot="append" type="primary" @click="copy(info.linkUrl)">复制链接</el-button>
<el-button slot="append" type="primary" @click="copy(info.linkUrl)">复制链接</el-button>
</el-input>
</div>
<div class="step-right">
@@ -346,7 +346,7 @@
this.list = res.data.records
this.total = res.data.total
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
this.loading = false
} else {
this.loading = false
@@ -359,7 +359,7 @@
this.instance.post(`/app/appquestionnairetemplate/release`, null, {
params: {
...this.editForm,
id: this.id,
id: this.id,
periodValidityEndTime: this.editForm.periodValidityType === '1' ? this.editForm.periodValidityEndTime : ''
}
}).then(res => {
@@ -538,7 +538,7 @@
li.active + li {
border-left: 1px solid #D0D4DC;
}
}
}
.publish {
.tips {
@@ -737,7 +737,7 @@
min-height: 450px;
margin: 0 auto;
}
.ai-dialog__success {
::v-deep .ai-dialog__content {
max-height: initial!important;

View File

@@ -232,7 +232,7 @@
mounted () {
this.getInfo()
this.getFormInfo()
this.dict.load(['wxUserType']).then(() => {
this.getList()
})
@@ -247,7 +247,7 @@
if (res.code == 0) {
this.tableData = res.data.records
this.total = res.data.total
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
},
@@ -322,7 +322,7 @@
this.targetList = res.data.fields.map(item => {
return JSON.parse(item.fieldInfo)
})
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
},
@@ -608,7 +608,7 @@
.statistics-wrapper__body--item {
margin-bottom: 20px;
background: #FFFFFF;
border-radius: 4px;
border-radius: 4px;
border: 1px solid #DDDDDD;
}

View File

@@ -142,7 +142,7 @@
if (res.code == 0) {
this.tableData = res.data.records
this.total = res.data.total
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
},

View File

@@ -246,7 +246,7 @@ export default {
if (res?.data) {
this.info.attendees = res.data.records;
this.total = res.data.total;
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
});
},
@@ -255,11 +255,9 @@ export default {
params: {id}
}).then(res => {
if (res?.data) {
this.info = {
...res.data,
content: this.formatContent(res.data.content || ""),
files: res.data.files || []
};
let {files = [], content} = res.data
content = content.replace(/(\r\n)|(\n)/g, "<br>")
this.info = {...res.data, content, files};
this.searchMeetinguser()
}
});

View File

@@ -135,7 +135,7 @@ export default {
if (res && res.data) {
this.tableData = res.data.records;
this.total = res.data.total;
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
});
},

View File

@@ -108,7 +108,7 @@ export default {
if (res?.data) {
this.tableData = res.data.records
this.page.total = res.data.total
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
},

View File

@@ -138,7 +138,7 @@
if(res && res.data){
Object.keys(this.form).map(e=>this.form[e] = res.data[e]);
this.form.type = res.data.releaseTime ? 1 : 0;
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
},

View File

@@ -56,7 +56,7 @@
}).then(res => {
if (res && res.data) {
this.detailObj = res.data;
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
}

View File

@@ -156,7 +156,7 @@
}).then(res => {
if (res && res.data) {
this.detailObj = res.data;
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
},

View File

@@ -197,7 +197,7 @@
if(res && res.data){
this.readObj = res.data;
this.visible = true;
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
},
@@ -213,7 +213,7 @@
if(res && res.data){
this.tableData = res.data.records;
this.total = res.data.total;
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
})
}

View File

@@ -112,7 +112,7 @@
if (res && res.data) {
this.tableData = res.data.records;
this.total = res.data.total;
this.$initWxOpenData()
this.$store.dispatch('initOpenData')
}
});
},

View File

@@ -1,191 +0,0 @@
<template>
<section class="AppResident">
<ai-list v-if="!showDetail" isTabs>
<ai-title slot="title" title="居民档案"></ai-title>
<template #tabs>
<el-tabs v-model="activeName">
<el-tab-pane v-for="op in tabs" :key="op.value" :name="op.value" :label="op.label">
<component v-if="op.value==activeName" :is="op.comp" :areaId="areaId" :active="activeName"/>
</el-tab-pane>
</el-tabs>
</template>
</ai-list>
<component v-else :is="detailComponent" :instance="instance" :dict="dict" :permissions="permissions"/>
</section>
</template>
<script>
import {mapState} from "vuex";
import localResident from "./localResident";
import ListTpl from "./listTpl";
import MobileResident from "./mobileResident";
import ResidentSta from "./residentSta";
export default {
name: "AppResident",
label: "居民档案",
props: {
instance: Function,
dict: Object,
permissions: Function,
},
provide() {
return {
resident: this
}
},
components: {ResidentSta, MobileResident, ListTpl, localResident},
computed: {
...mapState(["user"]),
tabs() {
let details = {
"本地居民": localResident,
"流动人员": MobileResident,
}
return [
...this.dict.getDict('residentType').map(e => ({
label: e.dictName,
value: e.dictValue,
comp: ListTpl,
detail: details[e.dictName]
}))
]
},
hideLevel() {
return this.user.info.areaList?.length || 0
},
showDetail() {
this.activeName = this.activeName || this.$route.query?.type || 0
return !!this.$route.query?.type || !!this.$route.query?.id
},
detailComponent() {
return this.tabs.find(e => e.value == this.activeName)?.detail || ""
}
},
data() {
return {
areaId: '',
activeName: "0",
}
},
created() {
this.activeName = this.$route.query?.type
// this.areaId = JSON.parse(JSON.stringify(this.user.info.areaId))
this.dict.load('residentType', "sex", "faithType", "fileStatus",
"legality",
"education",
"maritalStatus",
"politicsStatus",
"householdName",
"nation",
"liveReason",
"certificateType",
"job",
"militaryStatus",
"householdRelation",
"logoutReason",
"nation",
"registerStatus",
"residentTipType",
"liveCategory",
"livePeriod",
"language",
"nationality");
},
}
</script>
<style lang="scss" scoped>
.AppResident {
width: 100%;
height: 100%;
background: rgba(243, 246, 249, 1);
.iconfont {
cursor: pointer;
}
.tab-content-box {
padding: 16px 0;
width: 100%;
box-sizing: border-box;
background-color: #f3f6f9;
overflow-y: auto;
}
.list {
box-sizing: border-box;
background-color: #fff;
border-radius: 4px;
border: solid 1px #d8e0e8;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.searchBar {
padding: 8px 0;
.el-col {
margin-bottom: 8px;
span {
display: inline-block;
width: 100px;
height: 30px;
line-height: 30px;
text-align: center;
font-size: 14px;
background-color: #f5f5f5;
border-radius: 2px;
border: solid 1px #d0d4dc;
border-right: none;
margin: 0;
}
}
}
.addClass {
height: 64px;
}
}
.dataStatistic {
width: 100%;
margin-top: 16px;
padding: 0 16px 16px 16px;
box-sizing: border-box;
.above {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.data-item {
width: 32%;
height: 380px;
background: rgba(255, 255, 255, 1);
border-radius: 4px;
padding: 16px;
box-sizing: border-box;
position: relative;
p {
position: absolute;
right: 16px;
top: 16px;
font-size: 14px;
font-weight: bold;
span:nth-of-type(1) {
color: #999999;
}
span:nth-of-type(2) {
color: #333333;
}
}
}
}
}
}
</style>

View File

@@ -1,358 +0,0 @@
<template>
<section class="listTpl">
<ai-list isTabs>
<template #content>
<ai-search-bar>
<template #left>
<ai-area-get style="width: 180px;" placeholder="请选择地区" :instance="resident.instance" v-model="search.areaId"
@select="onAreaChange"/>
<ai-select placeholder="档案状态" v-model="search.fileStatus"
:selectList="resident.dict.getDict('fileStatus')"
@change="page.current=1,refreshTable()"/>
<ai-select placeholder="性别" v-model="search.sex"
:selectList="resident.dict.getDict('sex')"
@change="page.current=1,refreshTable()"/>
<ai-select placeholder="文化程度" v-model="search.education"
:selectList="resident.dict.getDict('education')"
@change="page.current=1,refreshTable()"/>
<ai-select placeholder="婚姻状况" v-model="search.maritalStatus"
:selectList="resident.dict.getDict('maritalStatus')"
@change="page.current=1,refreshTable()"/>
<ai-select placeholder="民族" v-model="search.nation"
:selectList="resident.dict.getDict('nation')"
@change="page.current=1,refreshTable()"/>
<el-date-picker
value-format="yyyy-MM-dd HH:mm:ss"
v-model="search.birthStart"
style="width:250px;border-radius:0;"
type="date"
size="small"
unlink-panels
placeholder="选择出生开始日期"
@change="page.current=1,refreshTable()"
/>
<el-date-picker
value-format="yyyy-MM-dd HH:mm:ss"
v-model="search.birthEnd"
style="width:250px;border-radius:0;"
type="date"
size="small"
placeholder="选择出生结束日期"
unlink-panels
@change="page.current=1,refreshTable()"
/>
<el-select
v-model="search.politicsStatus"
placeholder="政治面貌"
size="small"
@change="page.current=1,refreshTable()"
clearable
>
<el-option
v-for="(item,i) in resident.dict.getDict('politicsStatus')"
:key="i"
:label="item.dictName"
:value="item.dictValue"
></el-option>
</el-select>
<el-select
v-model="search.householdName"
placeholder="是否户主"
size="small"
@change="page.current=1,refreshTable()"
clearable
>
<el-option
v-for="(item,i) in resident.dict.getDict('householdName')"
:key="i"
:label="item.dictName"
:value="item.dictValue"
></el-option>
</el-select>
<el-select
v-model="search.faithType"
placeholder="宗教信仰"
@change="page.current=1,refreshTable()"
size="small"
clearable
>
<el-option
v-for="(item,i) in resident.dict.getDict('faithType')"
:key="i"
:label="item.dictName"
:value="item.dictValue"
></el-option>
</el-select>
</template>
<template #right>
<el-input
size="small"
v-model="search.con"
placeholder="姓名/身份证/联系方式"
@keyup.enter.native="search.current = 1, refreshTable()"
@clear="search.current = 1, refreshTable()"
clearable
suffix-icon="iconfont iconSearch"/>
</template>
</ai-search-bar>
<ai-search-bar>
<template #left>
<el-button
size="small"
type="primary"
icon="iconfont iconAdd"
@click="gotoAdd()"
v-if="$permissions('app_appresident_edit')">
添加
</el-button>
<el-button
size="small"
icon="iconfont iconDelete"
:disabled="multipleSelection.length<=0"
@click="beforeDelete()"
v-if="$permissions('app_appresident_del')">
删除
</el-button>
</template>
<template #right>
<ai-import :instance="resident.instance" :dict="resident.dict" type="appresident" name="居民档案"
:importParams="{residentType: active}" @success="refreshTable()">
<el-button icon="iconfont iconImport">导入</el-button>
</ai-import>
<ai-download :instance="resident.instance" :params="params" url="/app/appresident/export"
fileName="居民档案"/>
</template>
</ai-search-bar>
<ai-table :tableData="tableData" :col-configs="colConfigs" :dict="resident.dict"
:total="page.total" :current.sync="page.current" :size.sync="page.size"
@getList="refreshTable"
@selection-change="handleSelectionChange">
<el-table-column slot="idNumber" label="身份证号" show-overflow-tooltip align="center">
<template slot-scope="{row}">
<ai-id mode="show" v-model="row.idNumber" :showEyes="false"/>
</template>
</el-table-column>
<el-table-column slot="fileStatus" label="档案状态" show-overflow-tooltip align="center">
<template slot-scope="scope">
<span v-if="scope.row.fileStatus==0" style="color:rgba(46,162,34,1);">正常</span>
<span v-if="scope.row.fileStatus==1" style="color:rgba(153,153,153,1);">已注销</span>
</template>
</el-table-column>
<el-table-column slot="options" label="操作" show-overflow-tooltip align="center">
<template slot-scope="scope">
<div class="table-options">
<el-button
title="详情"
type="text"
v-if="$permissions('app_appresident_detail')"
@click="detailShow(scope.row)">
详情
</el-button>
</div>
</template>
</el-table-column>
</ai-table>
</template>
</ai-list>
</section>
</template>
<script>
import {mapState} from "vuex";
export default {
name: "listTpl",
inject: ['resident'],
props: {
active: {default: ""},//人员类型
},
computed: {
...mapState(["user"]),
params() {
let params = {
residentType: this.active
}
//导出搜索条件
if (this.deleteIds.length) {
params = {
...params,
areaId: this.search.areaId,
ids: this.deleteIds
}
} else {
params = {
areaId: this.search.areaId,
...params,
...this.search.search
}
}
return params
},
colConfigs() {
return [
{type: "selection"},
{label: "姓名", prop: "name", align: "center"},
{label: "性别", prop: "sex", dict: 'sex', align: "center"},
{slot: "idNumber"},
{label: "年龄", prop: "age", align: "center"},
{label: "民族", prop: "nation", align: "center", dict: "nation"},
{label: "文化程度", prop: "education", align: "center", dict: "education"},
{label: "政治面貌", prop: "politicsStatus", align: "center", dict: "politicsStatus"},
{slot: "fileStatus"},
{slot: "options"}
]
}
},
data() {
return {
page: {current: 1, size: 10, total: 0},
search: {
fileStatus: "",
sex: "",
nation: "",
education: "",
politicsStatus: "",
birthStart: "",
birthEnd: "",
faithType: "",
householdName: "",
areaId: '',
con: "",
maritalStatus: ""
},
style: {},
tableData: [],
multipleSelection: [],
deleteIds: [],
};
},
methods: {
handleClick() {
this.tableData = [];
this.multipleSelection = [];
this.searchInit()
},
onAreaChange () {
this.page.current = 1
this.$nextTick(() => {
this.refreshTable()
})
},
searchInit() {
let tempAreaId = this.search.areaId;
this.search = {
fileStatus: "",
sex: "",
nation: "",
education: "",
politicsStatus: "",
birth: [],
faithType: "",
householdName: "",
areaId: "",
con: "",
maritalStatus: ""
};
this.search.areaId = tempAreaId;
this.page = {current: 1, size: 10, total: 0};
this.refreshTable()
},
handleSelectionChange(val) {
this.deleteIds = [];
this.multipleSelection = val;
this.multipleSelection.forEach(e => {
this.deleteIds.push(e.id);
});
},
exportrExcle() {
if (this.deleteIds.length == 0) {
if (this.search.birth) {
this.search.birth = this.search.birth.join(",");
}
this.resident.instance
.post(`/app/appresident/exportAll`, null, {
params: {
...this.search,
...this.page
}
})
.then(res => {
if (res && res.code == 0) {
this.$message.success(res.data);
if (typeof this.search.birth == "string") {
this.search.birth = this.search.birth.split(",");
}
}
});
} else {
this.resident.instance.post(`/app/appresident/exportByIds`, {
ids: this.deleteIds,
areaId: this.user.info.areaId
}).then(res => {
if (res?.code == 0) {
this.$message.success(res.data);
}
});
}
},
handleSizeChange(val) {
this.page.size = val;
this.refreshTable()
},
detailShow(row) {
this.$router.push({query: {type: this.active, id: row.id}})
},
gotoAdd() {
this.$router.push({query: {type: this.active}})
},
refreshTable() {
this.resident.instance.post(`/app/appresident/list`, null, {
params: {...this.search, ...this.page, residentType: this.active}
}).then(res => {
if (res?.data) {
this.tableData = res.data.records
this.page.total = res.data.total
}
})
},
beforeDelete() {
this.$confirm("确定要执行删除操作吗?", {type: "error"})
.then(() => {
this.deletePersonFn();
this.deleteIds = [];
})
.catch(() => {
});
},
deletePersonFn() {
this.resident.instance.post(`/app/appresident/deleteBody`, {
ids: this.deleteIds
}).then(res => {
if (res && res.code == 0) {
this.$message.success("删除成功");
if (
this.page.current == Math.ceil(this.page.total / this.page.size)
) {
this.page.total = this.page.total - this.deleteIds.length;
this.page.current = Math.ceil(this.page.total / this.page.size);
}
this.refreshTable();
}
})
},
},
created() {
this.refreshTable()
}
}
</script>
<style lang="scss" scoped>
.listTpl {
height: 100%;
}
</style>

File diff suppressed because it is too large Load Diff

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